From 5accde0a96a3f9c3c2968769a85f0bd9f030acf4 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 19 Aug 2025 18:57:57 +0000 Subject: [PATCH] Updated via schema editor on 2025-08-19 18:57 --- .gitignore | 5 + app-shell/src/_schema.json | 7 +- backend/src/db/api/analytics.js | 366 ------ .../src/db/api/{instructors.js => course.js} | 171 +-- backend/src/db/api/courses.js | 428 ------- backend/src/db/api/discussion_boards.js | 355 ------ backend/src/db/api/{posts.js => mcs_pyq.js} | 179 +-- backend/src/db/api/students.js | 387 ------- backend/src/db/api/users.js | 12 - backend/src/db/migrations/1755629794386.js | 396 +++++++ backend/src/db/models/{posts.js => course.js} | 36 +- backend/src/db/models/courses.js | 139 --- backend/src/db/models/discussion_boards.js | 83 -- backend/src/db/models/instructors.js | 75 -- .../db/models/{enrollments.js => mcs_pyq.js} | 34 +- backend/src/db/models/students.js | 105 -- backend/src/db/models/users.js | 24 - .../db/seeders/20200430130760-user-roles.js | 937 +-------------- .../db/seeders/20231127130745-sample-data.js | 1032 +---------------- backend/src/db/seeders/20250819185634.js | 87 ++ backend/src/index.js | 80 +- backend/src/routes/analytics.js | 449 ------- backend/src/routes/{posts.js => course.js} | 112 +- backend/src/routes/discussion_boards.js | 444 ------- backend/src/routes/enrollments.js | 439 ------- backend/src/routes/instructors.js | 442 ------- backend/src/routes/{courses.js => mcs_pyq.js} | 115 +- backend/src/services/analytics.js | 114 -- backend/src/services/{posts.js => course.js} | 22 +- backend/src/services/discussion_boards.js | 121 -- backend/src/services/enrollments.js | 114 -- backend/src/services/instructors.js | 114 -- .../src/services/{students.js => mcs_pyq.js} | 22 +- backend/src/services/search.js | 20 +- frontend/json/runtimeError.json | 1 + .../components/Analytics/CardAnalytics.tsx | 142 --- .../components/Analytics/ListAnalytics.tsx | 116 -- .../Analytics/configureAnalyticsCols.tsx | 124 -- .../CardCourse.tsx} | 57 +- .../ListCourse.tsx} | 45 +- .../TableCourse.tsx} | 27 +- .../components/Course/configureCourseCols.tsx | 62 + .../src/components/Courses/CardCourses.tsx | 157 --- .../src/components/Courses/ListCourses.tsx | 125 -- .../Courses/configureCoursesCols.tsx | 143 --- .../CardDiscussion_boards.tsx | 130 --- .../ListDiscussion_boards.tsx | 108 -- .../TableDiscussion_boards.tsx | 500 -------- .../configureDiscussion_boardsCols.tsx | 113 -- .../Enrollments/configureEnrollmentsCols.tsx | 114 -- .../components/Grades/configureGradesCols.tsx | 116 -- .../Instructors/TableInstructors.tsx | 500 -------- .../Instructors/configureInstructorsCols.tsx | 113 -- .../CardMcs_pyq.tsx} | 59 +- .../ListMcs_pyq.tsx} | 47 +- .../TableMcs_pyq.tsx} | 39 +- .../Mcs_pyq/configureMcs_pyqCols.tsx | 62 + frontend/src/components/Posts/CardPosts.tsx | 142 --- frontend/src/components/Posts/ListPosts.tsx | 114 -- frontend/src/components/Posts/TablePosts.tsx | 497 -------- .../components/Posts/configurePostsCols.tsx | 130 --- .../src/components/Students/TableStudents.tsx | 497 -------- .../Students/configureStudentsCols.tsx | 120 -- .../components/WebPageComponents/Footer.tsx | 4 +- frontend/src/helpers/dataFormatter.js | 133 --- frontend/src/menuAside.ts | 104 +- .../src/pages/analytics/[analyticsId].tsx | 163 --- .../src/pages/analytics/analytics-edit.tsx | 161 --- .../src/pages/analytics/analytics-list.tsx | 172 --- .../src/pages/analytics/analytics-new.tsx | 134 --- .../src/pages/analytics/analytics-table.tsx | 175 --- .../[courseId].tsx} | 80 +- .../course-edit.tsx} | 72 +- .../posts-list.tsx => course/course-list.tsx} | 44 +- .../course-new.tsx} | 50 +- .../course-table.tsx} | 44 +- .../posts-view.tsx => course/course-view.tsx} | 60 +- frontend/src/pages/courses/[coursesId].tsx | 173 --- frontend/src/pages/courses/courses-edit.tsx | 171 --- frontend/src/pages/courses/courses-list.tsx | 173 --- frontend/src/pages/courses/courses-new.tsx | 144 --- frontend/src/pages/courses/courses-table.tsx | 172 --- frontend/src/pages/courses/courses-view.tsx | 403 ------- frontend/src/pages/dashboard.tsx | 393 ++----- .../discussion_boards-list.tsx | 177 --- .../discussion_boards-table.tsx | 174 --- .../discussion_boards-view.tsx | 169 --- .../src/pages/enrollments/[enrollmentsId].tsx | 160 --- .../pages/enrollments/enrollments-edit.tsx | 158 --- .../src/pages/enrollments/enrollments-new.tsx | 130 --- .../pages/enrollments/enrollments-table.tsx | 179 --- .../pages/instructors/instructors-list.tsx | 175 --- .../pages/instructors/instructors-view.tsx | 127 -- .../[mcs_pyqId].tsx} | 83 +- .../mcs_pyq-edit.tsx} | 75 +- .../mcs_pyq-list.tsx} | 41 +- .../mcs_pyq-new.tsx} | 52 +- .../mcs_pyq-table.tsx} | 45 +- .../mcs_pyq-view.tsx} | 41 +- frontend/src/pages/posts/[postsId].tsx | 175 --- frontend/src/pages/posts/posts-edit.tsx | 173 --- frontend/src/pages/posts/posts-new.tsx | 136 --- frontend/src/pages/students/students-view.tsx | 232 ---- frontend/src/pages/users/users-view.tsx | 109 -- .../src/stores/analytics/analyticsSlice.ts | 236 ---- .../postsSlice.ts => course/courseSlice.ts} | 50 +- .../discussion_boardsSlice.ts | 250 ---- .../stores/enrollments/enrollmentsSlice.ts | 241 ---- .../stores/instructors/instructorsSlice.ts | 241 ---- .../mcs_pyqSlice.ts} | 50 +- frontend/src/stores/store.ts | 20 +- 111 files changed, 1277 insertions(+), 17313 deletions(-) delete mode 100644 backend/src/db/api/analytics.js rename backend/src/db/api/{instructors.js => course.js} (54%) delete mode 100644 backend/src/db/api/courses.js delete mode 100644 backend/src/db/api/discussion_boards.js rename backend/src/db/api/{posts.js => mcs_pyq.js} (53%) delete mode 100644 backend/src/db/api/students.js create mode 100644 backend/src/db/migrations/1755629794386.js rename backend/src/db/models/{posts.js => course.js} (56%) delete mode 100644 backend/src/db/models/courses.js delete mode 100644 backend/src/db/models/discussion_boards.js delete mode 100644 backend/src/db/models/instructors.js rename backend/src/db/models/{enrollments.js => mcs_pyq.js} (55%) delete mode 100644 backend/src/db/models/students.js create mode 100644 backend/src/db/seeders/20250819185634.js delete mode 100644 backend/src/routes/analytics.js rename backend/src/routes/{posts.js => course.js} (78%) delete mode 100644 backend/src/routes/discussion_boards.js delete mode 100644 backend/src/routes/enrollments.js delete mode 100644 backend/src/routes/instructors.js rename backend/src/routes/{courses.js => mcs_pyq.js} (77%) delete mode 100644 backend/src/services/analytics.js rename backend/src/services/{posts.js => course.js} (82%) delete mode 100644 backend/src/services/discussion_boards.js delete mode 100644 backend/src/services/enrollments.js delete mode 100644 backend/src/services/instructors.js rename backend/src/services/{students.js => mcs_pyq.js} (82%) create mode 100644 frontend/json/runtimeError.json delete mode 100644 frontend/src/components/Analytics/CardAnalytics.tsx delete mode 100644 frontend/src/components/Analytics/ListAnalytics.tsx delete mode 100644 frontend/src/components/Analytics/configureAnalyticsCols.tsx rename frontend/src/components/{Instructors/CardInstructors.tsx => Course/CardCourse.tsx} (61%) rename frontend/src/components/{Instructors/ListInstructors.tsx => Course/ListCourse.tsx} (62%) rename frontend/src/components/{Enrollments/TableEnrollments.tsx => Course/TableCourse.tsx} (96%) create mode 100644 frontend/src/components/Course/configureCourseCols.tsx delete mode 100644 frontend/src/components/Courses/CardCourses.tsx delete mode 100644 frontend/src/components/Courses/ListCourses.tsx delete mode 100644 frontend/src/components/Courses/configureCoursesCols.tsx delete mode 100644 frontend/src/components/Discussion_boards/CardDiscussion_boards.tsx delete mode 100644 frontend/src/components/Discussion_boards/ListDiscussion_boards.tsx delete mode 100644 frontend/src/components/Discussion_boards/TableDiscussion_boards.tsx delete mode 100644 frontend/src/components/Discussion_boards/configureDiscussion_boardsCols.tsx delete mode 100644 frontend/src/components/Enrollments/configureEnrollmentsCols.tsx delete mode 100644 frontend/src/components/Grades/configureGradesCols.tsx delete mode 100644 frontend/src/components/Instructors/TableInstructors.tsx delete mode 100644 frontend/src/components/Instructors/configureInstructorsCols.tsx rename frontend/src/components/{Students/CardStudents.tsx => Mcs_pyq/CardMcs_pyq.tsx} (60%) rename frontend/src/components/{Students/ListStudents.tsx => Mcs_pyq/ListMcs_pyq.tsx} (61%) rename frontend/src/components/{Courses/TableCourses.tsx => Mcs_pyq/TableMcs_pyq.tsx} (94%) create mode 100644 frontend/src/components/Mcs_pyq/configureMcs_pyqCols.tsx delete mode 100644 frontend/src/components/Posts/CardPosts.tsx delete mode 100644 frontend/src/components/Posts/ListPosts.tsx delete mode 100644 frontend/src/components/Posts/TablePosts.tsx delete mode 100644 frontend/src/components/Posts/configurePostsCols.tsx delete mode 100644 frontend/src/components/Students/TableStudents.tsx delete mode 100644 frontend/src/components/Students/configureStudentsCols.tsx delete mode 100644 frontend/src/pages/analytics/[analyticsId].tsx delete mode 100644 frontend/src/pages/analytics/analytics-edit.tsx delete mode 100644 frontend/src/pages/analytics/analytics-list.tsx delete mode 100644 frontend/src/pages/analytics/analytics-new.tsx delete mode 100644 frontend/src/pages/analytics/analytics-table.tsx rename frontend/src/pages/{instructors/[instructorsId].tsx => course/[courseId].tsx} (58%) rename frontend/src/pages/{instructors/instructors-edit.tsx => course/course-edit.tsx} (61%) rename frontend/src/pages/{posts/posts-list.tsx => course/course-list.tsx} (78%) rename frontend/src/pages/{instructors/instructors-new.tsx => course/course-new.tsx} (65%) rename frontend/src/pages/{posts/posts-table.tsx => course/course-table.tsx} (78%) rename frontend/src/pages/{posts/posts-view.tsx => course/course-view.tsx} (51%) delete mode 100644 frontend/src/pages/courses/[coursesId].tsx delete mode 100644 frontend/src/pages/courses/courses-edit.tsx delete mode 100644 frontend/src/pages/courses/courses-list.tsx delete mode 100644 frontend/src/pages/courses/courses-new.tsx delete mode 100644 frontend/src/pages/courses/courses-table.tsx delete mode 100644 frontend/src/pages/courses/courses-view.tsx delete mode 100644 frontend/src/pages/discussion_boards/discussion_boards-list.tsx delete mode 100644 frontend/src/pages/discussion_boards/discussion_boards-table.tsx delete mode 100644 frontend/src/pages/discussion_boards/discussion_boards-view.tsx delete mode 100644 frontend/src/pages/enrollments/[enrollmentsId].tsx delete mode 100644 frontend/src/pages/enrollments/enrollments-edit.tsx delete mode 100644 frontend/src/pages/enrollments/enrollments-new.tsx delete mode 100644 frontend/src/pages/enrollments/enrollments-table.tsx delete mode 100644 frontend/src/pages/instructors/instructors-list.tsx delete mode 100644 frontend/src/pages/instructors/instructors-view.tsx rename frontend/src/pages/{discussion_boards/[discussion_boardsId].tsx => mcs_pyq/[mcs_pyqId].tsx} (58%) rename frontend/src/pages/{discussion_boards/discussion_boards-edit.tsx => mcs_pyq/mcs_pyq-edit.tsx} (61%) rename frontend/src/pages/{students/students-list.tsx => mcs_pyq/mcs_pyq-list.tsx} (79%) rename frontend/src/pages/{students/students-new.tsx => mcs_pyq/mcs_pyq-new.tsx} (64%) rename frontend/src/pages/{instructors/instructors-table.tsx => mcs_pyq/mcs_pyq-table.tsx} (78%) rename frontend/src/pages/{analytics/analytics-view.tsx => mcs_pyq/mcs_pyq-view.tsx} (59%) delete mode 100644 frontend/src/pages/posts/[postsId].tsx delete mode 100644 frontend/src/pages/posts/posts-edit.tsx delete mode 100644 frontend/src/pages/posts/posts-new.tsx delete mode 100644 frontend/src/pages/students/students-view.tsx delete mode 100644 frontend/src/stores/analytics/analyticsSlice.ts rename frontend/src/stores/{posts/postsSlice.ts => course/courseSlice.ts} (80%) delete mode 100644 frontend/src/stores/discussion_boards/discussion_boardsSlice.ts delete mode 100644 frontend/src/stores/enrollments/enrollmentsSlice.ts delete mode 100644 frontend/src/stores/instructors/instructorsSlice.ts rename frontend/src/stores/{students/studentsSlice.ts => mcs_pyq/mcs_pyqSlice.ts} (79%) diff --git a/.gitignore b/.gitignore index e427ff3..d0eb167 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ node_modules/ */node_modules/ */build/ + +**/node_modules/ +**/build/ +.DS_Store +.env \ No newline at end of file diff --git a/app-shell/src/_schema.json b/app-shell/src/_schema.json index e5e1950..8467a92 100644 --- a/app-shell/src/_schema.json +++ b/app-shell/src/_schema.json @@ -1,5 +1,4 @@ - - { - "Initial version": "{\"iv\":\"drSq2rAyvEMcFoke\",\"encryptedData\":\"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\"}" -} + "Initial version": "{\"iv\":\"drSq2rAyvEMcFoke\",\"encryptedData\":\"C3qzJWgbBEqHGh4InzwCPDhaD2Q4aTeX5XxSmw7aPOch/5vMODjK5VyL7WzngK0dJPsxQFQHANYwSaGVsB5frfSkgGA57+mdDFRGjK1VU2OROcx9cT58kosI1kCi+NiE8cfL/XYstObxJZWgookZuIA+jcw18JkQlmbVdZKmbYzOqKixdPw59TlvZgjA3ET86CLONM96pEwCQwd7qH4pzaj6qukT4aMtTzuGFBeV0a+u5ymjm9e9gZ9DNFjMmcIkXMjcfNfTkvXB0oTy6v8z5GpvNCdyfQHf5xRK7OIU8IHnHs3REgk52iEgd9KLmCBkFaNMV6bBBW7+blhfrQ9vh5vzAPHSvdrBzcOD7W6X96VIJOlBw+w/u2/eJ/8dVNzDibZaPfZ6y9YSPR7SVG/2PYBAFRvAXLfU3pjv8y9xzQAm4QTu079GRPRbE5sAF/pneX/HwXfKhiB0rie2UvHGCuxvFIqTJ3xfiQ+HB7XZJSou7Vw1bTeQaeSl/u126g4BjykF+I37KiOPl/5HT2m9cuTSS7f02qG2ABf4/yRPaPaqYPcvV5dl+X7ERO/8Brwib3g+GEAI8QRvucebPVyUg3LMVD5BSbFlWPAgh5wykB60OQprSK+KyhFq4ZNqa1NpVjaWcudZ9XoRhf68XdRjbro1z8imJyET7Ox8HcNy7lB+Jzwlo1vFzrFv2J3L1ePvupKfFBdyaUFptjiRoaykaZaM5gwtlsewoScgS3F52X0fV1K8QByu9Ne55nCx3HTNFanNDYuaBR++FYjijpWJhluYSjoKm0AGSZhXm6GNYTj/m66i9VFA1XnhHBtJeL5bfABCX2O8fQtvx8/Qd1JwmXnJMooAkDRWMU9TC7Tn+U6/0LLZttjP1MjK+PvnH+KkbuhdfIMI/AYpn/h8kRcrJ5N70Fh5SdiSDq7wFlhcwzXejicOQWqkeRL8zcS41OQObF9E7jWRXIuL98QArAAvUoilB8JqU5DOAqKaZYM3rKkBJ44ANBFPg4cVyQJ9TcRgXgUEO3kuvs2JLVIPNI+IUP6sPXCG7tTZHDTNYItVMiRNiuidWNrl7OT6D9zf8rmC2UZUz/KPgBpxhKz/MzB5+/pC0SVEJIzrjyIMQwedTmXXO8jeWneem4qV9hYDV1Dcn6saNU69c70AEnvL+MebrNK8WQJ/EKlSraJElwabBx1pJ+VM/8an0mi3iDGIrIPRKTROUXRGdTx1vpH0S2uSDf4BnY2ZJVfdLViz+wTEZsjpWkwUQTzME4FfnCR5k0hOKCKFv3K5oqIAhk8TcqMR/Bbof/HTs2E38UctbP9j1o+xaaSaiuBjrvg6XO/D5M5a0u97L74wRQIPeKfdTeRZvBr45VJpI26txPTAFMiC1WQdOHcGRiaFB4Tmynck0HxV0sRWI8Vvrwa9tntcIdnNmWf6Nd7MyvrCTvcV+4mwIZY8kwwtHW/9AZ8drRKvkydiDjRgDlIT4EFuEIhHp0UeZmKU29bt3s3o2Pjg0C99/TF4mQSvTSwLY880ndQRjrAW57Y8QrYKVaopE5zGdQwLHY2DcOd4h3fVi/6mHggolKdHzPj+YibPVzH+tUTbaGDH9dCbXpfeFSpUM6jWvN70Fksbj4oZyRr4pH10eC0/GL0+vyPCnYs8IV3C8TfliOG80n4hb++eTHfxTTWCphdPSQYNh3EDTMYdYCd6N1PWj7weeshR+S2GETHtIXt3SvUqNoqKZdvfPoXbTRKBa77HC3pSFAJSVyiD4A1aDsI6jR6lGyVHN7LsuLzG08D+WmaQFjQrCAeSPpGTok+5grViNiVsUAmbYtH7EoHuGlmXIT8j1nak/GreQDVOIeaTAE/MaWufK3u24+UHjYHLLRy7cy3TBbOHxseYE+mh5PeaT2t5akGXSIGAFu2OE3A/hH3cH2nG0of8fSiGfYMQNMy3BdQ7TFaMakqqrWVce2vxFhhxIGjZrBQd+/EwollF5XgEVXGAU93fJnWTqvHs5A+vPYWLoIyBAwmLQALZN3B9sLtQb5I/LX1ki5EOHGjSKA4lCDI4mZDeQdP+fphiyy2oIU+Bo5Wf0s3NlPWFJHde+JRXG8dnhlPDSCU9Lh+1pvR0RS2OQ+i3gMZsTNm82F7R21EuPoXJo2hQaVpxSaF0uSC6G758gZvQSjOkRSI9NXHFckamE10uMWgZNZAQNRq2Q3WAM9EmgtRBy8DWI/EhJ00RVCpJuNQcZhWA9te3OTaR3eM4zFt9dg63IW2Un79QZQukdLdd6yfMoWoqh9OtrwaB2pS8pvWvQ6resmPE87WL6ozsfq+lilGBdCI4jrXsGGKICT1ufyxAOehoeRAHnfdAI1RFgJzWAVwUTN1ZW2SwinsxNMAw2l4+nQrX2N/f0wfOeR1vMi2mu2x0t/WckZWNmMf0LtyyHARDC/aYBWm9spe6h/nKWhPLFLW7U+yOJgcIiZzmv3fxisoWhUQdvzUOMlmCShyCWHaT0Fv4qa0x7VQIuIXbR5iRdG0hqgy491BQ+bOAtiJJPKQeeY04/7l+NLGJBJOSpo8apucGD7ISrRjb1A/Jaox4GeU35ACGtTGcgp81h/YJsO0xoNDxyZkvwo7Qe80BV78Ykx4JfXlB+xmCLRPML1MOD8tg1oJ9p3mrLkhQS0MFT3Hm/Cted85Q/RT2nDuy9a18elxUQv1drZT/JvfEAc1KoL9qZW+ULrbihxibUfqqX5Cuz+HDlwHLQcHkYwgf2sMOyPLrZ8jLqffH//UXbVce5YRkkiteG3yE4A/GJ5on1r8YhD0b2giDKNxoQiS0Xl3MaQbqVjARjTEQeA0aapau6wTLANwf/OJ2vbXHFLz9AAXKET43rFAnUxAnxPeciR8BjXbOzy4u4N4dz+FOYtJkrp/7jH8kijUK5YIdorpCRc2Ebb3uk0Io+M03iXBkEhFEtjALMlUIlwOLgxrAZCtA8UupyQWpe5cu3YzkP7JCGhLXbBP36E7OpMHJsujcIQ7a8zxyprjmmy160YWnF46Fsz2uOPdRpggwaN8yoBXtB0MgGNqf5TF0mJkaW5/oX/0LpRKV1KCE8En2xlwtqEL4/J1U08mJAlffRZ0HA+GSjptIowlqV0DMmHYYViTVWaztgCZo+4lM0dwbFhS57mozdpRstJQMrVw8XUiTVzn7+GRlwzE4CIzmT2IGjH4il1NA1ZO8LtT8D50b9WtLET5rzTwGGcvHBftGMzCw+3WathnLOETA/aLcOWfqu52KCTj8lT/EqIPL9be9TK3tKnC2y+eJRzKw+Ng3sGwDMVKvQAjOcpi1Akcsgf1WsjO4JIK20X7n7x/+MmJpMGcvjwrIvs0ThmQW5QaIWhT0rOk5hHdUf9yxsvA0PXsFvJcFvdll2BC48IUaKL97q/zJ9hwM76a1vRUttDMbcyAqUDjbKIwnsoqAM2qvHMT+SDiV/0nwa5FwJ2np26bWUYvKvGsw0ecghkEkMLsfF+3OT+jXUdkqLOGktFzz1RqkUrSMBpheLTOo9F1GMK5jZZWPOzXFs/f9FRry1A4rD/bf57uPynQirLKmxU2Sd7J3TCrPjZUs3TrDf2wKqDGwH79xo4wLF7OvDi8OEbJrr4WxMbtYk2iLjpsftNAClzHWM01S4jrZt9euQlFTxEOqWBCRTPzrtXnPEwgQym7bvAZeiMHj87mQkiIuhCZDuK+s6q49NhxuCDmIvM6be8hga5lIL2eV4tglsFa44BrwKh2mCA23SIbiNx8b6Or/w3NjdQzSH2MFKl51hwOcwgpD8vMCdY0un5nB3FydTDR9roG9ECt9CRhoA5FVXQDW329DqX6Fu7meeVxrHUAgh8v78PB+fNGGCfkGS6nzk6o9KM/uxMIXjCDgd475c3wrVB0Bcp7oz0lNtqSVsD/4/khekndv8M8+Hh3lU/1Co3XOF2XCJkVEDjGhN+kfC2ldv0xne9Ki8v37xGDZgDWpkozqUX8mU3ObuxSLQyyF1NwzJyNRbvsvo+9NLp5QA4XPhH9mUbhw7bB0mo90v8M0ZWdVj+30SZpuNLShnPWmub4Un7B8iwH99ydBgBqG+TJj6tSRplVnCX8FMGxd1tM+E5R7ukSIbjQWDjEzXtwmrTn6ozQsoThPmXmXeO6Sz9Dt3yMinZrDYBtxgybl21s9agTP0YLUEU5M6+KnGjYBAtvtVDZX1lYABeDROyg/h/kJXA9ihCbH+ME/CEM7FJVPISndZJBYq3OwOqCXODwdZd+86BLjlc1AtgW3+495zkhA6laBCxhRo6Z/V0r37FcKWqGwm1PYtwI7UhTOJJ1aBS3HB5SmYS1WVUisQim7KETR7MCtIhW9WWKuUa+DV4V8+K+0SYJVY2Rvt7YBFwrYc9ugYjq3/Pn/U9Bra2GLanZDpziKqkF9/ziQWAvwsCPkTLVQydooYL2WRvwjGz1khcPaUGj1aZmJf5brWqlPuAtpCilW4UcwSPtuEPAG17tVrBu+W+PfXVSgj5d2RqrMGbPonLBaTpRhytVaNSVK3CuK1Rlc9AtwjBI63WrqmEkstJmOESDR6xQYmsId0uT/6yQpIEL/mcoPTFy/HlPeYj0/ua1oxq1I9z1N1CF3uFjFy9vbEXyiRjW/bHzkCYOxji4FWPOQBLVMEBsV+gIaN8WSAt5wqN+4NkusseFBlfd2ZbNNAvOj/8IglAgurunqfMF3Eh54PyMZLuHIeKXHRHUb22jgWHGQPaWXLAAeGhv4CpDvZtq1pL2k1h/EkSq85CutTLvpWnsQk4eGuce2C0Y906hpby70SVrPqHQFQEQ1SAKm9zYYdcMoCXTQalRC7un/+Z/V13otBJT/k82nTnkLqiWMJwwpaEPuXRfENokCETSY9/QRFl09bvJEnLou4GHGAyRjhIroAsGGf59tVWTuAwgr3IMl61SAHyLmAfpT9/XWvGb5hYibwGbnMbic7F/l1pGp/nto9ECxsk6ezYwLLra0y8rhXS1FfvDq+FyPNkMmCwOrXzuTA7oRa3wfJeQ7t45g9t227weUKMw6UCg3TVXCX74QPwO6uQQa3YPOLrMrvILcnZdvmasuEIBw6kKm14DFo+QR5gAIQcIljRRE3oK3vQpSKJQ7V8kD/EGndDp+YmpAYb3QePtNgLsOSqhTkQ/otdSBnS0Z1OXmtp2OP6sy7h3z7MFBHsYNcW0z09/wAup8ENhDfPGIphhxB2kQwu7i6fpMhQQbJRvVP/ufIFOhlnGacxMFmBdj1fr5pTm5Y55em+MNvrLqphp32pVAlobCjB2Boy83oJXDJWZMcXVxxGszQUVVJr2GQ+sT4LmYRWJ4c23jytecq/v8bQwBpOtzcGcPXbeiJO74zqtjQMYazeypEA4r4JiCE6FM/xyN2rRc8b600pu63Nw7R3oyurEKTJWIT72FTZI99/0gw23WU4wqpADX8VAb8W2/Q3M6SOI6JjalTmUmSNo+U3eRnv/0Nuy9V8iJBk5RV/Vt8tzCdhYWQEBggdlzZF+DqezK6okTibp76zAIZM7/bwm7NZHcu7D6Fe6yjfpLVMgh5EKvEb26aY4LNQRUlBnhXawJJk53eNpd7BuCNExW3/sjdYaCN/Qp+KRG5XSMmj33WwUf6Ph9XfvWCgUC6QrG4OaDWZ+70YeTJZdB2M0hwlqLSpWXL2J0rGTKPndozk950WRRxifqvmb/Ux5rGfxFDM4cZtUapPBj6X+/NYrG178dt53YmQMvalc1KR0l/9IPlE5h5OFetaQk37kzecl/FYrSDT5ZgsIU7UIwje7thNUTmejmBQG1xVcSdYM25UjKVEg8qhaFvY54aXNHMUVF+ZaG7nt/uQk/lt2b7QcLf02B5+KyjIQRb3eqwYcfGm9Fz1ZbLt9OCzVjdHm0kxM2dxnUmFbyQwLh0DjxbhYIER+eccVgIEzlHJJGNVY9GBdJhs1JafGv7HiqphjBh2mzoIdNCWmIKfWdb4j0tfjf/O8SUBpv4vfcbHzYayU00HqZHNb7QZHiflhHI/d252lD2MzayLRXDjhfTtWyP0YidjSx8qHYq1Ewn6ZEbtBiSEML8SwWoObPTPJc4DbhBpsmFArrRCRCljVi8krl6jIne8Ovd36dxwPwz9CqNg21nNj34tTzwz9OIBU0ElLXdQIexp/MR2XBVyg+qO19CX89W/S0CX9DYaNMtevL8dGsvdn6yHLSvenFgTIfx96hBurbPnIuGmS4ayHj04b80328fNcoLoKkKV3aheI8cO6tKgHcjNlTwaUzFOlH/W8OmoIkih4pVG90aBKWx72b/U0BvK4xCc2gQChTrSrWhKA2RkWKaSY2o4cmOEllQjia1lL/rwtzhcfxfvGqeWzVmjb86xVDrCqC9QrRWCPpx+1801k97JboDf3NFHTwmHAqhJYqBbedD2u907t7uAuD2mp1W/yT6veXOxRfVla7ob50kNRBE2BFAnOSR1hXINnur3eKjej21/jLCg9SMltJ2e3ugcnzkbdcfwr35/XaD3KiP0N2g9d85kEjA7LWHWgf8T8gEGrY9XcShafMdRoapAQWVGNZHjBC6ZrqkAh83eSNY4IAbGWrn4+BanYxKF45Onym0/bjO6faIzXL9b9Y2ZqNe8F+rVmor2faEl46y0ej+fOQjjvty7+aMiAlY57JvSCc2vKL15X3KPdHDgytUk62qNlkXIyyBjdKanxxmfdR09eCeunR0XfeXfRHyFH6Ab9J76etOdM6El3OeIxvn8i8EVOABwh+y4ha28FZgIRFGyvWEOnQaSubpYwG0qSOfOh7d5a0DlQB8J/a+KIgl9a+o1Fz28mTVlhE1FKrHog0A36T6LFFbQpYdxbfU2b23fML5+9Spzr/khxOvBeM0cFcivvEFN2zs9BaMSOcxlx0s+HHVeYG69lcQW2IXPJAlf1hXWEKnRXvvywBXqS+iiywPpq8wHUl7S3oEy4N/0EJ0PXQFR65rqA9z8Sf7o+5L2dAM8mxATiE2xnZ34BzF+4hF50z2uTrG+o24mif8bgIfuXur0/+dy1dHp8ZC07YzDJw9k/uICYru2Wz8AvfeZTkneYx+dvIQq7KXXtnnNduH6wcg2jLLKcEX7bbDX/r/BR/4hGxCaLKzqhywx2cmD3yt/BZ1Lz2XMRrCbxyPgDnIVxHKlB/E+4n76E0gfwZD5P6dSzokRg2x5VlMwo6VoDFFo3njw8fa/Z7yh5ExTO7cvZFcT/E5Wzqc0s4HEAQ47pB3VgpdCH3ww6nQD11LZDujZ6kojugPNiHYFjzt4MKRUtTjmLSMFaG5J6An834zltPMs753yuglL/YdXQmDzm/fLy7E2B07jr+DZN/K3f4ShjB8NRVahB0hYI3BfhLsYoNwWHNSqKZYLO/1H+nVqaIEgZEM8GNmhCdkFh4pHaf9OkpB4NCgrUXnBJpZ+qad91cdLvYqIDV8tRE8NLP7/aMILWdQuhjA2kx9RoxSCZIExN8j3zNxUfT8LGyzEtkl62BNyzkKOzizKP4AlGDjggxOnaNRO0b5KVRlHp9Ap8T8Z7cU8IfAIBo3kydWaLYcJmg0ByD67uYMziz8ll+Up5v57U+GL5IMIFWjmW69IAI2uC+aOrEuG8gmNGOCFfy4blY57eJ6tnhcsRBBAEEELYTS6gs5d8biq4XQlDw87eqXIkrV2SWYUIj47S1XSizIDADaLiC01br6P2JbKZR/LIT8bCLd/LfYfyQKORvP4jVIR8a+OrmAeUzDlAElzukia2QzP2Ryd3fFXMhdtHUSNP6kzF+fApo5im+jqGst4OV6fLtV00m4qQj5iptNTi1bMcvFONv67Q/a9KdjJLH6Qj2UFe/KybR/t06zAdhNL/QxXWmhfdWHnUH8f15VoC1drtXBFnwN1p48C6J/cZ2URWLarThQPnPYq0q80Y9iuTRgJ2vTwsz7ir+HmQjzuAGjOZm5MiX8XDeoUR0nDjAi11RhRnMxcm5oq5wyxOwbPXmfZo1vTp64JUUQck9Pk+dQHc07B31XKYcas4iqDFWFRDo+B9FN4gPx2KMfq2+HazMOJTg966NbYP5IBzxvRGwX1OZ/EBhTGpYcBqcWNQ0cjC45vGB8mfh0g4GavcO8m0mkwPUFodGzUA/apYQnvHKE84Pe1X1VH7wEaywCLb5WdVtlJTP/XBoD/OoHQaRtgru65X9OD5Bf6vivRMrAwMo7VcQofmkyhPH6ha5scdc5wOWlvuHntVN4DPPMT+30qjlLYryqU1YwacLLk93xwX9ReIDc0Jo3Sa1KoyO1OjtSDqzGNAtRvsZT0XSK7mkX4lqYFxKdQesHkjgeSj4VBDqaB/S9+cmn52TMq089OdqPi/84CO9B83ML2649WVg/WjjI3nugbzDbiqCsuMD9HJunpSBlZR/Mp/LK0T2Zba3ImYWtan/w3z1ZCDZrt8+kaPZ73qfGjvpTwx+3YmYBjmCMX3vgbvgjBgkOqOuFFhe35MOkIH4DwqNv5ii5DL7Sao3xw/2uk1NYsnLgTyvBFJYyH4MYhI2WckkfGAqNgj3fcX0Oax4XvOV75p7uVSkeuRIklhaylKKi2KuAPPCDCnhPRGo84yO0m/gE+MVDQ/I6L/EMTJLJjSYQ9Vre5wGI78e4OYq7TzjQVWpv278Pm4yrhA8f2f3uAshV6N4j60w34rStgTOW9B4SbAmVOHcsIymS3zCThaieccWhDJC86ExFups24mdNYZVyYZcIdqrj6KYWQo24qVLx0f+5IhWm9lYhEE5h1RhnlLH2q1u/ViXEd3BDfXtB2x+iOGv9fB9du/2jsTHmSO4ITg0o7xn3wU5FDj7gIOXgVJ25a+zlSIlcAv02zsyEVB+1lJbibpOC8gfOEyYRxQKDA7H71AXDO9PmRFPsLrvgB3pmdT1xRz5bBNaiG8HLL89qQ7k1oqB5o1lyaQgVyTfAcvP1NraGgsescVAPRE5Rsq0mIDSdf6sHcAkm4xd8ummenyiKkrdEs35wj9/qKrXOn+9zxZPL13447cGhw7VV3lr39c+IQUQ/A8Qxwgc9gd9Y1VMrld90i3SggRDRePteHrJBg2CCesA/Ibq40jFRLUaOv58t215UHShzm8eyfD1g5L7sWB90RqqijTCtYT0FdFl5K7R3lzDxxmkxQuWtq78a6T3/O0KqPwMlXqG9YuUIiZYj1MTOq2NgL+/EFDDJkrvRHjMUQ8mAXSIuqmXGLjLKAWsZXQ6QG+HQfU6ou1EwMOCFKbyRVgmFcmEOCHLmHjZ1NPmGRa8LqgHos0/jROhZtCgThbwobq5yrjrtAWpGS9vvhHOgCwUaJ6TU3Ptp18ZQTiHxcd5lq1WESiCh8OPUaFIdhhOg3Jp7/5H1P7998irS+BfP4o/8NxeACzosf6SNFFJG0jk4UvZW2KVpUVBddEXM3QXGcwDAChzET8mB4Zlwr3aNBsJ4RvjHo4aqHglqx5KKQrfXeBG6kbMT/2YvyCCsHnbGwfCNkTJh9ZyVur/LDRdhdjBjuRrp6uIE24VZTGeJZmbijvZ8qyGCw0YHjkpbPLiUGntGTAv633R/8f96lGJzx0hh8TBF09ziknDlC+BGwBHvjLef1voNWKlBKfKvNoeQRyrJPmQBdYDrA7CSgEOYz3CEEus1xWCwRXtf99At+kbJRpDzVuvHmaejwHXqOrAKbsaBR8cBV1547P06V/NFjxceX68o0DH9+wcbqJyw3+cAC/u1CvGzHiuQrf9oGbTvY0Et6gm2GxBO0EExvSM65/OzFsph96lC/wRG3W1sE6M92djloA7x4CI4le4hG2+RnsJJDU3qGANeTEpH5CT3Jrun+0fkPzUledqpNVsqdChp/83F6H1k62C4vTTs6xGC5uL2aup4Xtjapn4rwEBpqcdvLiljDRw4lRJRajwLrTAlBhAAak6dMLpCf1pXX6xl0c2FrZESvrl/RLEfiZLZomYBjKDrNOpjkGdABlSdAhfL3ZTxJHDqb8UajmxNB+e1+EAyt5F9aqty4t6Di1L6YTtV1IqoJPdnVfFC40KLXN18eoNLJZbwSzCGYvgYDoBw2KMun8M0nCxxvpk7G61UqRnnXEvoM2f6PF6yL7j+Mu/aNzAnz5Ar2MzI4F1Q5LelhsmIGwKdGSeYtDBqZKePUyuEQvT7QRIfmsauSWTFW11legBxeTiLxiBJJRKhdFQ0MZ5MUpEWdSJHgWRW3wjFQ/P42JMyyALED/vbAY7/u++FQ7jma9IrRdBVVF8Ky4oL7DlRZC4PbZnV+5o3tYkpBArO1nix8HTT46EUlfQikOWAmJhrh8ncaEGstCON9VH9Grjr6O/Fd6psJfSrNxnfoOWUuvKiopOMFUkgfkmCvbx6HjweQfHF8zPJxcnOndputzk3El171uoQrFqpMKfsg8sJ9fvenOFEfY95vj8tPm6hwxR/IYG8AoLDsk5CoM6Jawoodzq35Cgsql4wslnbh4+fnz0Sxd+Jvniwoj8Mxi04FF90jcqGpSn/igjwp3DfESBc5DacfgJO4YJEwCN8vHBtUfTnzqeTPf+2/l30Vj9nigeBf8npzbHQxfRujUCLxlLZUP0DMlSC3ewbI5p3WX3wtqi4qMNzdDmZphC4Sw0HbFXVE3GwjM64ak6z6IxBziLrF1T+rkK5gQ2a59F0QuGnOT3mwtLqb0l0+Et+vCqk1aJTmld1YTGxx/oiEb1RKBIG3gQY0SGQKiEZ4vdgLZ2eocy2ReK+hDCB4jwcMErW7pnuLgsuBUgywqun/zftKpD89ELujrycwMr+s1ENveZbsL+AeLB8ijqS6oDL2WVBddHrsVw0v4bOlJosPNcUS26ZOB+erDjnekHazQK34TsRefL92yoKKPe5r9rVP+BrEvV7nqK/4jRJGt1VKZaccGeX9HbhG5tafzSjrKL7sPmRWOanR8i//3bRFe5NAQSKm+dQpMnscuxcPOulQuzxe4pFo+NHW15ViHQYLjn8GZjwTKmEnbcD0e9axnwgN/Geoff1c/zyOJ2X12VnCR7WqZ7aJFjND6JRPr/6IalYHzM55MRqj5MPBVR6CV1sB4ijySF8NCjYmbTCYdruVZD61CSvL2VJOK8KFpTiGklgQlbgAiVUl1qhGXBE716mdnidAj+kJgIRZ00RiICvHBx+c2XZXxU4y7CVOsZmXnBYqWMKpiMhBaFShoDNuPXaechphEaAOqS/Tt93WD74qWnZW1JjXNgoiOpYsBQR7p/GA4tDN0SsKcqLL2ep3ivQPM3wfuUg/ue4giZOr/LFr3bB2FeS3BihO36gTo0haGuc1IXv9S/2xUE7b5o42haqsELwlmPYxmj+FIAecv4HSaG9YWJFqUVfKMNsvloqg1tLKF6PnhwYoHSCjY6BhXewX16uQvafkhF7PT8T78m6FPEmjgHpaiiLMIpOwkSSC659734i8oY2EFkAx/iwxoClpPzbmbHW43DzKakrYCzW0aXVl2eWsF07ccQTURENH51r7Bz3bUYdHYuzLTLYhVLlr/ev4zMpJIPyFlrzHUVZwXrvOn5HQnX2jPNQpL0pdPogTPL82egjJm9C73cne1fNYqvt5GNV6wRQf3dnwjjoX8UpcPEECiOFeg9AMd3m7fWXrai8kvW3gYVLj5SE5DJB0PclkOKNVTEmEmD070UmLNZshREA9chIyhBN2jh78G9pKeoGrBurr9z9rbr3xvS4pkbZ+QDXPTWTArzSEGfISjonzdkO1vDSO7P1+9mXKMaDwiRLRHlpBFGIxg4dHgqDEqYNrGDk1Wt862VJdszS0P+0waXB59egyPPMXJYr+TxD7g2N+Snr0A+bF8PDda7qoJZSnGxDMspHa9ooNLY+QfLP1ZAQTjbEYqnqHRAAYFKsCja90faPDiJMBiyrsVn4RLw3tq1z/++r4/8+0ylgm/YwO6dArjAiS0j6uv5heQGDsiPishpwtI7gYCUy78gvtRgfgEewAL+PxZowMDM8rMgdF05d6Ka6qfzUzs8YbQ6gn1uokOPjHK4CYk3PBTfTpH1S8byImg677BM4iqFKSmpptJ5EfOC3lPa8glaerYBC2HNJRJ3eaA3RdjCJGuQKLVQ2ZbxFLUFiVgP7vX5SY9ai2VIXkAPjnN7MBa7UqU1ETqrXWFj0qQNZ52k9q1lVCKPn3yQqJdI2+qPoxjPY9NI8oA6mdZWy6JyG05gYX1/m+fEZ/WAkNP08UJ+KeAP92T7dEtjOBzOQtA7wmWUbBYtcc/x8wGXSzpjcQhSeZeoGSjQ4EnGE0jqUxdxm44i7fInLvTo2phTOU1odkuTd96uE4j6OXL3MB12qeFW+8/M+EzMC/nixnsMNofKJW7SNmXsLwzRU9OUmr+/FvJruTyZXa/+VKr4Bb61Ix7FxujK8V55aofSSVj8NH/Mx63NNfH7MOffrO7gi0NP0jz3jAR9HxgQXsG0ukyD7PF9k7QGp8QTlTX3C8H8oZJlaO4yxk+paHnjo8DJ/tYueEaI2Cdu6d8I14+rD5Vz0M/yQ6Z0i8HWF9h3e9zBxTA+foSyG32PZZw8YHN4e5phCU87Jqqz7QnY3eLNxVBJKHLHlbTqr9WjHIxAacw53d/QUo1EoPWl81Brl1mnrsHMXy5aGG1dlLhCAWxFws2szkHVjvBB5Zfblzgj6BNLchq+wsqE9Rqn/xt65ZZtkcTt4JSP/cfAi8TiHkkpTuFHKlVHLMb1rkJ+2vM3GDMQrCYyhWvJFDzKDqM9APexfAL4CWaeN7MWOb+GzAYr8NhBDcz8XBs8txwuPdMjNkW2E0TX/yON4J4UzFAafzT6EqQWag8Y7+orH5rtV28nT59Nqrj9/tayrjHxfGpX+5Av+COSfQn5Q5J7VVPGR6csgkuE9373Q49FW+g6wGg2Kz7p2s4nLONbvNR8WUNj4tVmIyEUV/jlF3zDfBjvMMYf/Px7zKFHpS6fUfdw8rnjktac9FP3FUDHOf73FfliCHekEwtBVV0YBsp4CCp6HLoPfANRg+lRhppStm8EhlX4cYpasJQqmPgfpLfxDdkLSDatEhLd/CflzNk7V8cdMod3TLtd3gFEfNKXRj0ahG9okfxrKcRGu3WNtpy7pt6hsMOgjc2h5SUlA5Xg9kQsE2xzT1/8GMpNdqWznD+khVNIr/AecwMHfZgECLsxkzVBzIYnH1AG2P2CSIAUsjz7K6JhzpjPonAHFZMIZYP3ThoxzfT+ylJ4lddFKE6lZqFIDjjKWOBGGYB/bL53EuQSc5VswHCY7QqdHRQIz7NCyn248HmtE/p193ltEw6tXxHareBQKqCQ7hfVxuMYLJqACu9knxmv/vpXsPNRzEGc8leyRBPNle6Pcq7BteR8YKn38cjxMSwWz2ihkA4pDhAphvQb6wPE25ta96aWLjPEVzTTw13uEUFCr8sjUhDs1TBwxCv1zDXSnJEc1yi3cU3lY1m08iIZRUJptZirAgyc2bvXZBYlmOrS2BFbex8fjxUZ09/1ZRIcq9OgK9V2TWd4kCJ7yI/Uimwfx22gqpQaAyioiILfCVe2pkXgFSML8lSB6m758KhNXrSSrrCbg9UW3p4oSfpUi6DQEiyy1K5mryyViFxBjctsINYNwquV9clGNAuH7ewURFYNpmDcCj0RrWsmiBNaTXrtBKVVAOFAcZQ0LijcyDzJMzXuFd91y3b6b4TY+w1uL6i30AEbNYD09JzkGsSVukuYRCe0W5Cxj4pB1w8ZNhsewU8tzMhqTU9UPpjvSyPwqsi9mGcyi5wS6RqvQ6X20jSgh844lICeQlFOi9sASRyaY+cJPLTFQva1Zfqkt/Wj2OTNwFmx4RfTpF1GT1sbI/sUDWAruYb1cl1CRx7Jw96a/JhMIA7Cb5jzZc2TB0zLH/lC4M8UmZWCM5LXcA33VJmG/PbwMl17ABx2e48d5AXst6xVYpcGPsbhnxjZdUWijhE0HVgyf4w+iMzmuzKCsqzsdL6IsxxORpQ/Z8iTQibGDMsHLRZoogwMZwNygeRMnlsSg5Hh2V368oNVTaLH4U2oNSvhD2EGDn2ES5HlM0HfCAaOPkFqP9zRxHV5YPnpzhnRxkeKxQdXHKl2SFJ+HDyStfRk/5SZHoaN3Vqmyg1nrVmH6kVBRWosxzWaroALMJxgdYNGq0tHX7DiT7dBZKfZ2y2n0bNBgID7d9xmykpmCiXCq1W8XyE2pRJ3epemWp9B35tj4DXdlsqtF6t/Xzu65IGyZP1X+dHhPP37LWbEmDVj1UaY2DXCAiry7z5v23JSSqePHcPbXEbRPipM81BI0vawW+MsoMjyoCxPR4taviDTyocnmTQyUOAfiXqdtgv41G0WCKKWnjxZ6YEIiAa2qalt+zMpe0pbVvDh3ALCnB7tUM1dzt+0U0dJTQgV9Avm1b9KUnLn5L6Cm4E/wYKBcVz7nYq6I/1KSLl4eiSiE+8bQ16nXsBWYTxf/CEaiXFc1RwPe8FAXYuhDZFvXV1bF48QOWd1SRMTy5W1YqRoSblgffs2/IjY+/t+vkitetoIDgqUWLhAn9H09a2PtwLbPbTVIsIorfZvzPLVkr4WxQD/Ju/U6Y/hS8eTsTvVQ+RjX6VXDuZLl3bbgbtM16tt9w2E+ZOzfJ+LWzPDHcE0F/H2EOPL/Hq8ogkJQYYvXXOyWLZsdhoY7FwGA5v437ZpH3U2HdVujGgr0vfrcHxo6hIVTk61/747eFajY4PXx0JJh3Zh3wyyg7zKQYlggMeXtH5F95V7ypMYZWZwrfMkbawOy8DZKeYE6P2Jn+d5X3vGp5X8kAgLhRaU4uRe60ps7x62RzQkUGazzTlv8h5BSq8ldCFkZbdnbtR3j3iHpzaJUo7fOV1CX3Y3gpmuTCw9JlZEXIPHnO6TYQwmf+U3MvQ7LDCdfpjjjsU5zg7EDU14RMk5Lz31Jp+BsbBPnLtgPuxdj2sLkOtwXsQaq7eaKU68EynetYjyWPwSvkba1w9KSAT26c8qnLQXUks4YtCJcuvjUwo3se6zYT1KNGbXERX318Eic8N4LNtIxg6MRcBIPJ5ywuWfGry1ssBf6Sat9DSb98TzniWAwHI3a0SrafJ5aOAKeLMm3lnQu8ZhmRCKFnRU0CiK+5WRlg4mcOjh5IWp09KOeLWw4PT4DoPerEC/pVjnnu0LeTeZD+ISoCRFvU6nD+y6HI8oYfDHY5J+PuAFBMkoZWgbvTJvFsPVkJqOp3EfexVS89m44wdsVjoki7Y+Qf17H67PuBBUCZ1dS8ZshthAbcDH3beXfOW4jtJb4ljfycXGMjUAZoaOtopiZ8FptmC1ntLYKiBi9WwVweUcCnOXTZVF6uIFK5FbApC6K9AZOOeEb3XYfLL3ICRwKNFp2tydpE2gGShgX8BuLQTWQHpQFIWm8tpmXV+zODYHKSOPXSLxiqNyunoiF6tgmOsRSlPE+NooQOgbuMycbYuD0uy72exfXWzsb0MSwATdrG5uazuHNn3COdCoek8mBs1JjX6qYil7PtS9z78ecQExb9eYYfLuVPq/ZZiMvy66I4lsrq4G2dkVV1EG0GBC4swZL/yXFuvdu9sUFMcmECcuWlm1IkhqqS6QbIZmotspYZgb+46p5N2B/gxRplRfeO382ycD7CutQBT1S6dNxXrpr8jJ8uxHdIxqCOzZ+W+AAwVF6VrTQU6IU2ye/r5Atg95TxFj6KQcSAiQrLAuKrEdIfyFm6UGFAn+d3Xjx4bUUtyYsm8iUMTMTR9LHQkr4PGmul097t1UMs3p73AA+kWjX6W7AaKkQNMJTCOQQcvvGCIlZ13ctBrFTuM7ukyoolJU/Cnx+QVH9A3QqG4If/T6zgYU3g7LEUMiX4ZPLbeycbWtb93IYoJoKrbEQm08kDkSX9RSC3DkkyAi/Ho9zzHl+UDgYP86yCoiQ7RV2ucwuu2Iebru9Yj+MhmGY+5Yf/mKBJcjLWPytXq/KkLgQ8grHgHHfg48Jp8E+y+VtoK9muuyopeKYCisnp6ukmmOV+F4PQxGxRGQbtACeHXs/lrtPkyuIHiLA3StpVkYg6rsF0Y5iK/nmSbscJkk33BB/RtcVcHEyp6EIJPa/a/aLoaMOGAn4soEETU2+obsT54nZT/+aM/9xbpse8BrEKIpf3GuNqVGvCLxOy4KnmY7Ey1/+HHim5ilRRH9XAsFkE1IGLADD3rwdkSjkvwyrzfvG6I+Ih3hTcvAFh5VDojOyh23tt5Jh7zUExGFZk7zRLqF8gW/jGdRsQlywROkiJZNoerUuZX5gh94aII7B2GSQ45ic25A82JXG/vC2CiA+KOEx9dzeLRh0D8Z0gVbzuzNGipe+sOxP5AZfl/vayg+BgT2Im0vQsCArzqLs2nE2A6GNm3vNYb0voAhuilltGNzeRqqxAQFEhkNtYgbgQCUkXqTEveOaii3oUK4GxRpSndV7Rg3jK/q6p8oL74C2ONb9Blp/EUQ4y1IEtM+Kq22e7SSJpzqvYGUu2XZhxh3kx8nXF0hz27GkUyr8eLyopjJZAmxYCBOc91kJwUIHT9AmRIh1h42Oo9l/jfkreL2Ff/r7g/UB1P3tNtRryEdzokLVQUTJD/D2JGFRL7haswpmNILp3FBqPDrn0R2wDzGQz9EC0dwh/wK5rQNWep4egaQ0KsfLvQTTgLqMvAlFfqVuzxY7S8xIVLT5FDLlRy0CSYDqyhnn+34uwVLtPWKVDTpA03ajgigftgpFTZSYQFDvktt169KqcjDl+pchr3Xyvpnla3XRBn7JaOkiC/31OdD0tvjNtrUSmQGf6uOhM3w5nkKd/gIzZrthl0vV7ilh1+D7hkmKoNNqlxi1MCxm4HSj+j1YCkjWHb9vc7teBWENMtskC9IaYKom0XD0XYVw4YfY4F6iUmRlJbD7ubley5j2p1VrpFa8CRnAMqAKAyXXsibb1owFbYF4YqNJXQeHoNbt/QHnv9IdSoDvyt4AIDLLt1bX6EDa8e743Q4DdjtvC9Sk0b43/Dl2oyo+SVaBj1DPm7VNwcDqTd+MWCmNPcWQOrzkRr2s/KsD02pLo1nbq5dwmRcXKqeoxl3ebmsLQjU9sbpP8pt4YJR+4U2tOOngS6FUIbac8b1J0IS/cnOrcGvhXTqZjiXKr+PMdsH5G9UF+97GQwm1TiPmRK1SENeCAAmYePCPMIs4HJkcw/ZE0O3pFHDl17lV05+iq0DmD1brsk9X7L9KO1ZCIwBwB5c2EZCZwM5Oecqiv9pnaOR+60V369T7Ld8MWEhHNPKdnq46zRYEsUjgwQRMFq9IlkA7sLl9ONkUkdlondRY3bST+r+MCwD9x1mTidnerwMh/hmbAF2JD+56ETr/ALpV/vXmN1v0YKniZ6CCqWp3YOefRVxYPm3xg0zj4yRPFW+5BKpfDfCqX4sIFW3vzIA0UZIi6OL7wHr2CHrtWyGi+hIU8AQp68kAeI5cGN6ZKCedinSWikBmMeyGAmvt6z5BJ3zdEl+5L1MALprvzj+YaSeQwLCpcfNSaZVjnoJ2YDYDbWTYeAzXp9m8Xm6U3OGDgyQO1Mf7FFXwkwsh5AR7jDIhLCvHZwV2E2lhf9MCwdZpaOYGuLgMwvC50ehoTgRpyeRjSzKVj/BfkzwLlYix/Z8Z1cHHlheNwudNaxQu+si2x7BJRnCurrZN6CZgHRCQIXLF/DIDJ9mVGuSbKROhkU1nnnOZTG4QwvMx5tlAkmTWLKxRc2AjDgiMl23POFsc2w/YU392aZwrE6eLkTQaYH3y28tA7EQp6oDLJIYrn57L9gYmGm0KG/rjeI/5bWqoIbHE1MMdwbqyHKWvidO0oXITlVfJKXargVYP1rcGupIlEROSpqHI8q4LkSmqh0O5j6QFXdUP6YGm13Pr8eEUDJNzXfihlu+inTahmHVwULqyqyXL7g6/tcOLBcGawBSVqpXGzGwqY+/eg4kPmbdbAThHcGzGlxjd+MRUhx00wH6bajkch7CrzrOQf6AwQZ4Z2LyHATJCKOhkqA8h6nkdY/CZoiFLdAC5IBI4zuGKnQlgvrztHOxXVkIB1wfdcYOHxy6quX1qA7+FgNEJOKkLmLZqJ8SM7ZE4OsZjUxkaEPPXhvWBLb1I0TK13jzHHF/99Vem3+6rqFCtHBB4AeVPTjG10ne7SNVZGSCVdZ19102GzTZdZE2Yjgr082veAb/z7VWi2TXoWDtCRT6F917JftSOWiBeat+rmGwzayt7tW6td/408pv4q48FywyRe788YdD+n7/nmVFWs364qntTZy0myvEdgx2YCs5XpmaWGmLfKpWGxKA5fWlg8E90i7iF3q7vkIbpkRKpQUVxwOZf6CLFwd4GGNi0EDjoZnd2vTofeBjIdN4/E0pQyp8Npyftrib2KCETLxI5y4HvLOocykv/aOJlN9ykXffk7+d42X2OYxwR2OM2NAtOHfmh58mFi5ZcEj6qHMlT/lUN3cpeWN4THqkzzPHRwLDzZXG9T7XZq6TmtAAPkoSWn+VxRHKnufp0pnmdjJla7msJ1mefR8xMw/FF0mWrbHLKipSX30xWyyoVDVKOe4DxVI8Wy5A+1FElno1Hs32N2huGH1HNXETuzCx3EddKKr0hG+X+YWFrGpsS9bsRHlSLbcnEq3JiEtTWW1KoaFWs0ykPi9Wu/Fp/7G7ScCwMVQ5xdfl12Rj5pnK3woz4/mA8J4+fxiNRjts1Emss5qfYocB+GGoN/lgyXwyaC+btbH+ATvR2XwXZuLxSFNpVlqXM9gX7dteQhn2+C1uLbr+BMskv1I++UpYU9xsZtRBCXZX6qSsLRLjqFhqAFFaBqiFn8Uzg2DLX6l879C0PjWazuBRnOC8oW0W+GrBDI9rOsd+LJHNyIdwWnc7xjuVyiNxuVps3UC+osL6ZF8wuo4Wqukm5PQeHsGnMUL0kTR1vFqBQjEwveUviTTBYVFmzqHb0ZHbmPO0qp1nmx6GspAJ7JERwHGh3ZcmV8ONSkomHssrKq5KAbpTDBgVEbh9DKwh1Qh6cEUOXr7hMP8nwoB19Cj0+qcLcyVKJuGa4o5Rfeci+y+rCKNYFiQdOhuIAxSdFifFO1b3RqaTyWzVRDj6uGWezZr7ZA5nBp/gruTr3cOEQpZHipdLs2TGIm5Zl+IM3XNFeMmeM1luQP+36sCIA8YP4jUA+agJhHykoTbiWkPyS6pKsWDrWWj/DTdm05zesWT4mfAvF8uknr0muiDheI+EsO6uQSnRQ/CfllmkCOJ+RNv450Xk8rDTwUlKnmhoWbHTy9nxGFcBvS82anK6QO4QmB5LUN3lKbaG23UgH6gEmxsOtL2ycIJNRC/EdueQmNHu0c6mH0OhPTIKQ3CAOcBoum7g2QaIwQS4Jr5f4EvuHxRWwp8Xtzg/nU2lDvd/X20JsLH/1be9Q8f9Z4XVgn+LaGg1VngI0GRdCDpTs7Bwiz12yxAgT54wJkhR/W5Ej604txf19lBwNeSVuUGuOu/grP0syTyDidnrhJX+ljCCkUiK6gRI3A5dGNDwDZkhfgL35ndBto7F3mQVw2hr6dJ7Rsjty4Imx0QfSkMpu51a91ZVb+aSMfQLe+KgSmQSTT8wSRY6Ww77sm13PBqesASFi+2+8KGyxs0GpAPRjAwkPyA9ogr0ipgGpsZSEPEJAVm1qpbZgjE/A4ASjCQ1+q1Q2N4cX857ua3mj+FKec+UYni+0+blzb8ivn6rG54v73gphRGrDpumFr8oaEWOBRzEeZiVcEDMtDZlqU1IlUgZrQ2aAhEV03vIRb8x9ih5Pn7Q/Dp9wT6FecvpI/Bb4dPTXyOZF0d7gw0Pyg0J3eTzuaUFjHXP0XBGStxst6K0q8t94vBQnRWlCrl7voFNLXltGxPOsaMXqqFeRo7rWyr77Ca/+p6LuiCAqvXTaRzKCyamvyRvoDVErYMn1Li1Dzgke2Fodnw6/mvGqgLivpR6pcXRnF+i1bgm52CBUd4L3NErYII2Hf6sSNYvLQf/5Z/KhDM9oCmM4f2i6jiz3p5ca35LbI6XbM1N2bbQXiptJZvJBx8amjyYxFsuTjIuDlIBF1+t+/R2PmVv+gtGs8JdgAkdeYy9yTDcql9XJDjHwQffGy/9CL/j6UAv3wl9hQXyHC535kRNSxKXzv8i8DRpIS+zF+5wTBHBHX+PS0QoL5sVIT9h/hWqK9+OwtD9iyooTRM7wSYtGGNOvgCtBAFyRRM5CC4tutLyuZNc9sZPgT6O5QCTAQitMY3DRVvfsf5bg87vT/ah1FrGEuIuyntud4tXSIF9mJRDWb/Q82wdrtIDNPky3oh+CjE75ZXqqwbObif0zESGGE+d0KMG0XQBXxv3WBxRbBWc6yUYz/omnel1X7g2GEinGNwmjTuRTmNtwkK0YKWAiqMoxcvT+YVLIUtRDNP9x0AgieD/ijdedVLwXDBqA7XooDaOBGv/1xFK3Dxpxv6U1Can5s+tDIAfhQuSulv5cr78KoTArVxfn1xr6JIs/8v3ZWaa7RsVwQ4ae8Q06lCfNOSAvLb7snzkOLCl2Z5YpTuUuEV+oYpTtnKSg/ysRpJdozDrJo/9C10QXwB24awXqrScP+7Yw/G3//4HbXPNDSB0Ptlc6dN9ju+5HgYQC4PlsYLUPOKAuLJY6h0lVkSZtvrij9LC6/cBl7pO3niu4mXCvLvP1XwKnQmVgKPR/+OuchBMussECw5bnBcXTgskXruBYceasA/PeBtaoM8t1i+FkS7SQPFisFA0OmfxQ4idb6PnDIDdiULnQzcfjZCmeH7VfNpxqgFaPY91d8ZqJc7o0exMJFFcoEmfFudktihjsjFPOycGC33t+GnFOdVVWwFC4eger6hFu4K88Zlmc1WQAa5VGaTGxFxqO7PDXZW5IuRALA/BbLvmgpi52quX3vLrIXaEXX0NZOI2C9xSjhFpZfc/1eUQVHMSaK9IsvHJ23aUaW9FXazzLas4HroN7q1HVA6IqPoLqc9gdSv9/pSFMD9Z9Qj6pgUdulW5pit/gqiQOSMmMQV8NaRKhDf9a0ZffATbTCJd3ukoZBkPFsfz9XDs8DB0b0dwu/2+00hsJNbx5aLZpEMqkjWR3lcDK3XwPxixoieM9A12iZzG3lLH2HnPO84GxiiohkC3e+I3On6CAIAV+mqOpD0QMHODQa781n3UpMIAKeIfjNYPT6kDRlYflAgRZjDR6Zf0bPST2br79oshdXdJOGGgZ4LdCyjl0/AD6j4SA9GxdWD2gbh4Vypagbf8soo1stxL/XqEZhOFLtqWjseu7oudlmVmdTT+DfjfTrzoqGFonuSgkzJKmz3/AvYtjhkKoOF0e+PqseZfZe9WzxoDbPTP4SouOcy7kYVTqMcB1Q8SvU4BAf3MN7j28wLAzomwPvuXTmd/6FjtcPRYbC3O3RCjN88B9szDYTm8UYVv7UP5yqcSKVK7JCkhL8xGcN4tnnLazi5FKbEL4l5+vGt6JB1i9FpFMF0OxXejJAcKwUGNIZkkdC6JSngKd7UbVgKTw0Wq33c5FQT9dcFjOq7615XddPTQ3WYMRfzsZzaMY0FTpXr10gOf+c+u/CMuyHY+9XNI5XNCrn98odlQ51NCxpuVXC4F/8tIUMvJcCejb/KIR7hVHO/xJZpTjz8P5qP2O94V0pFq3TRpYwlhzlgyTwRFrDDC+9yAnToG4Pxdlne6yRrTUoVOSs4wruTLtzuWRFfougTXu9LAcjR+u78DforYc9KBFUeU3JQf2E7eDFlqXVnQjeydNc3H2dRVAae2/7WOCwG4OulM6ZhFKt6Q6iD01hj6x9Ce5US7p7NvwIYrVE1l6kRYHDRNeu7eNHPSp33aZwsZjkqPuElK2/WfOz1hjnCwml8TBgzZGptiiGo4BuA9Uvw1k0KrwuyCZ4RF6/1Gl+79xrIekc05Vrwg3/yOUKeXrGdUGVxi0HyPTz0E1ZiwWCDfonBm8/HfbHg+emEmv3aVcO3IfaLByv6kYCC9Zl2a/uMvOuQVpooEMae6dj4k132KV+jpXigOWVZsdKvuijN+b1rVpIqcRQsE/VJNsciW/Icnhvy/Xx5Ha8SsTtm3vzJ+C2Ic5t1Pc79ftgRQEFabnVv9naurqHRnrlnCVE+habXO4KhuY53g2OzV4ObOgFp/ov2Naf4IvWQy0QMoHk1vjk/LIIDpN+ioCXeZZ0Gy/YFewhQKnizF/9xX8nyacJ3//mKGtY7PGQAV2zgY2t8NlZdShhUfvLIL8IoiLv67yfHxSzyBwKHoc8uN/d3tmVo/Ptzc8VMAChBzY70Ml24xNig4Z4+NGxEDCDdiHABbGJSxSm6/OjJW4Q9V0kRIgN8i/szAk3i1OIdBLSWvhrf8w4zLX6P55rs3wIvBKd6XiAbwJB2JUxjz+DhOH/zeXjXDmBSG+Xu7uoR1qe+EZbKd3ANfVjmkYTCH0zIn/POW/qn9dLh6klkQhsEW+rKf7vHJhzdYje2JHoRUOAr/v/ThCBg3iD4kLDO2X1vTAup6k36RHOUJzbBih4o8p+zJabxLsICictRtJbvQ62CGK3yAaGOLU8ljVb9vo+21NKFo4dlmp5r32cZpBPxqR3WzIzPwX8AN8q+F2Abu8W1IdyyFRxvfJgE2+SWjNzZKTn8myh+0NOsxAvUJNnVnLvWPE4mVhhZjsI6ipQ/kz+UEdnD3d7Xa9M2UbRRmKxmFKy/E5bxRGN4hSvwzeebVszCx3nVYiqtM9UhawirFiU3+yNXkzg51AajcaD35bo9F1S/OOIceHir+Wfmsrry2HXKUuvRalwqpgU9ZXZROOi11mv40myQhP6Nj8bsDUzvlX4VO+gH0Yy0J+Wv4sGFhCS3Bkt4XLTfTXMSIb8k2utJSVT4idmogB2DSFlSmWO04h1WKeFTtH0KVghNQ2GW1oS0ShrWsq4h+Rg69Jk1iqY3pA4N8PkcPvq0QvBNX2seu09L43NoA/oGQNDm2el+4qJDzElmnWNBJ1bpxzQYy/Z2w9+5zk7DQhqJHcwqY6DHHR0Q/QtxWRfn72vUWhzM42Ig83Vc4NFZGlz+m+Np4+wIVMXLtK1M0xZ98Zpe6VibD3Gtgo5nVy1HziXORrRkL5BU8c3Lb25ZO/bxFIzEQlhZAUObUEpvJ3+JyZLGBh+D3xAT3StXFREJI2QdjYzAsH9lNyLxo1zCERv+fsxLx8/RjpF4Ya86TXDy9j6ngGIjGF+TAFkZn7cNlZaMNcqb0/+xfF3to3eRZrjhyc6riyVlvWMhPtThhhWUiu7Kf5i3sqJM1FgVqGaA346q9luOMIyFYirlrp+gtwlIGtSv16BGf/Yd8g6P7pnjxVhZ39ekgm6nzo2+bDs8qArYZmEznhAWFEGeKxC5U8tpbUsDjPmFF6yGJ3zHBrBfkJuFy82Wxc4MnRZ2qmPo99HvG6h20x+QLkNjZKezedijVyNjMmYlO6Mx3ms0nCT/bHmNSbPHPD6jklifIjaKmHqr3ilEHKAgxtCf+bvEoxKNQoPEpM8NNw4HSvD2B5Aj1moe2ClSEdWOPWbdEVehUJ8bIvMqfXtsIYAPvGUwROyNOOZ/Y48jVRIafIIAkj1GLvrZt+439Mze2bRYCDjfK/OEZv6NntsOXzq+Ik2IioEye7F501yUOSu0ovt9wrEun/9Off9Cub3faHPBY9rrUdYdzZhfw/k3e/huwfTyQQPa5cHH2RuqpHzY+SNXaP5E1PmbZESnNFa09YGjusynVdzxksHbI2aJvnQRMzkaryheuIrN5A58W6cri7IvyWndy6JFVTe4kRHGgowt2IeG/EQiE94EHs+2qAoRt0oT1XBeSt3EWDItELmSRkGYD5ivy69jIbxWfwB+LhwXI6XkpAgFh9ta5dwfmfnchKAqusVxID+x55zR6nv3zzJwrtCFrB2fx+60bei+JRW896MEsIYsTQSY8h5w4Kn/kTxd4lQERn/S7KNdJuJsUNK2/PN8onqnaNlpwdQvkiHienJqrbIR1HKqQl/g8givGNdZe+1tiQYIapxAc/DrA5vfdEtx4HQ8nqPywg3dYcQ0XAiGanffp5vjqUlaKtqq1b1m4CUEwDbJzM2UrZu8u7oObBC1RElu7RQrMVIajgm1f8ds9or5//89JdswjW+LDle5B6/4mvj85z9UWLAyLKcxYF1UCXrPx+Jeqt4Ekc3tl0X8pPjXKdw84VXDEnWz3PFDhqA5+g5O1Gwl84Bhb2cOAmsQnvFB5ovT47jYX8GNKKe9u/kp6JATuNx1CR7PbQhkQYlGWn8lpj0jo9U5Qz5UEU6rryZbKEA7R4cLsiId3OD0lp9yr6BlaAVoZwQF2jRd1mQCpcP0doZlBBReoQQjhfC2rDY9yxF3ty+X1wx9TPHBzMvYaGLW4mPI6uluIPInzUH9lPmRqxhqyfWCDhUXfOEqrlkg6Ys6pJCsw2Zxa2QuVDCfTZGrhXaxpZlxSt3VaYynZEkR4yvYk7v9oYU8C5exo/voE24ymzRvIoKt7L6RZyG6vAOCWyD/Six0fGqdjLWBDsevNs3h0bS7WFKvA5T4/B+lbCV1B7iO7VJl7DfrYRFLxLjJJ8VPCG3DPfGxWwzOkqr5jov8eJ84tTp6lmdV4eKVGVh6f+JcqNtjOeftJQ3AV0NM0A1Ryg60YOW37aDcYNTqXfUGj2a2uDv8NwsgHKZSsDmOgsU4NN5BFGcXzQ8bX+h5NNqOXxuQqMNPoO00QNJKaSANOyud6o8APQc06pv3VSQC9kU+G7EzvHDBhx//UeAIOrUUatIz5p/Z4tRXIvVS9JTHQYepgXp3xGHpE09Df/rAcTEtNrd8my1m3a0dv+6Q27Gr4fyx9VJHfGl9nAIb5Rmbf365Hub3GS8Lxzf/W+cteNqsH8DM8Rcm/w37f/XojLesLsuCzzVcT0l5/imsDRVXPk+VYo4Ud4FKU2l8hF+INi2hIm5gmyDraQAwz1mVnfo6i3CberZoK2rVFlUm8o8WS5xOOSpTH40ky44/0W4eyCcLoBK8KqMgIsts1cJnpdF5Hx7dMpYeUQXeah5NvTV7pWqtXLpnh3BdZeCbcK0tHrXkaAakfNbFQC8yzX6YniB9HLhlTHlbkLvGCjZKoiv7Y724L/axWZs5+FDeecpCBiUobb314ATcZAPw6T6e+cfmhDRy6llIsEMuoDCI+mL3pF3MvjjIAbtQ2fLxLTOxy7yw3Dw8fceiBwV4Kk80TfQ60jNEChdD2CrQiECHTrd7nM8lMlDMe/Hq1q70dgjWMze6OJ6Qb5D+i39+S4myLXAKutX75UT12XaY7vexW4ChMUB97imYkVqd1IZfWssT3nnJIpwkqJqP4i3n40xLmsyMcMtg2+kTmbXz/PPAMPQcmUI6+peUQb5ZgLQSOZEMez5ocMMcWurZQKPqVWE+oJWlxrmylLxcj7dQYIaBtGGBta5EHxIAFCTOuO5awaMm+132kUOFZHAqkRVAjqRVNrnMyI4janJs8qtzy69WJitflixphrOF3Im6m1PIwCywp4J75EkusHDLQ/G5x8AShSV1tBCVZ5yrwn4nVUn+iLWDC0JPbjF2vUcxazgXHr5JU09+mSvscFFXXymw5gM2keUimkfVyDXeJ1bJnWViw32rBuL47dV5YKzl2Ng04OxpQwK0mRKqH1pQM1Tc2GKbuZyexzpq1mp8mBzhHKRNCL8KOOXfN2WgnqssSJ9X3Z0tZV97rmxxLIUHHVmvE3NTHZu9wQURBiAuqUWoOtT6pLtA4+B1HqqYQUhWODjWh8vJrQfQWu5jMg+RpKGuyAnat5TMYBneKhkrzB+NxTcO0NVDgHbXOC+2BCdsITDibEEQSLFgHVMkudYYhtnScADdYEFKS/lK7oswcbgOz/DpGRBJPyfGxMJC5Yfb1AmEn9Q1sL9+A9viFUVmvbH9xL1j2MXJvg1S7sVwzdQDV1eq9fhvsgLp5ftljp5nWNdeOcMvgo4c2LQEvsx5m6uYbpeQtTJl/n6y+i0tVuiKZ4Naw7hfUO9pQrvnCs9UBT19EOgMjHwIYieya7WAhBRliOop4rVYAmXDrh4LOArfeBUKQtIixqk7H9tHu8LdN2a9uj2FsF/UgHA7y6Yy2gog0nm44VcsFxi4heh/CbC3P+LYbkk4hobsl1pft00atZ9iDcjxU6tRREO2oY7vx6EdE1goJ5XY9C9LY24dsaPUhor44WwlMtiWyWZrhbgJccF3QQIZOI7qBrFb38KeLjTwCZaeUfHtXf3YlLkUDdoaQ84RZ91T6+E1zxaFEV/iWYI7apvH96FdVZAfAdWHhLUXH5jpjOGgY6ZLSta8gR89WIoLYHuDqUGg3ffLnd5BodEvGutCiyYcZm3c9DHQprb19CHgd0G6RKdD9F7gUqeLyR22fPiBrxxoSvd9xzXNmFVVAJivNope5TctwfF0nJezTi9vY988PSZP9+qquvuO4Wk2D0PL+9xgvz7Pnuldqv0BdNA+9EOSfbCz7ndK+plhy98vRqYzS9QbZfTjo9cK9J0Cs4suOViNLW8iN2CnBQi/ux2mzLu/PaTv7Zq2BGI9Wn17s+V+WJRJyG6dl8Cemr24VEvXCMku6fNCS0kA9tOigcwalrnglC6xTZlQMvMxPrcM3e5mY09kFhOqS/u0PKuU45xNvVlYhgkLfAtuzYpa6aGnDR1AWWplcoC0phgKr7ix9smsSqCnWC7Hk7v/+YyfsrP5DEpLfO2DSL9hShMjhcNNE4lXZyN6EYqzeO5uXFXwqTxPifF78vbM6p7UJm2Ur82pjBglw1Re6bzpvHi/u+CuBSxBYwIvbkqB/ECvSnhehHQQdn0U5DGgKWxvJ602UOeSfmxfAhPjfeOYfYqbl9PdAzTTkBqAXuqc/masYfbc2j78QM8XhobHqA2MvmSeW0L0/iS25p/lPV2fkq3lURDDgkM+GMHKj7kzxlz8qoA22VHssuZWCbxXH6IwnUTWw7r4dGHbwrhb2wvyNhFAUlABjshbZp3da7yWtHl5DyTBnArjKtetRxw8wnvS5Jznq0EZyJgV3GRLOIg7KeShYsdWGpcaHmKZbS15oyNFRqXQPAI+vWcRmEJ9eHK4bc54=\"}", + "Updated via schema editor on 2025-08-19 18:57": "{\"iv\":\"jMTS6YyoDjatTO+z\",\"encryptedData\":\"nBhxuMCAC4Qiw14NF5ybukUfphbjWSOoyAmktHrKM6s3rPCyCJekKHl3i7wbMOTeTKZSF1qlgM5UemnzWkrJZ1k47CJkS5Ncu2KmVTr4002Ijk3eY+KKmOK/9DQnSB+UMmDRtA6+AbBStmVGhqHLjd9erxuBiOr7diMEOQuCqHaCnnF7gIIgQKiMCXEOokdp5FC/bTbc+rQGPArHhD8Qz8mvcrjhCuVY+Cg+SHCkFcwjVBM7z8h3DKE8PIqNz/VoLvizsD4v4qlQnS6VZkQLwKh7s1Xj3JmmlbHY8wX4Ix6nNJQ5XHaPWIgNBSBjRHssCinbUec5Hi3XVJMi5CiIu+q+KjUZyLvflnA4H+TvJ8lKMNYrW9RqwkAZCc5y6GqzzIA9hwONLasylxiJ2tHUBUOxP74pudV5yorF5SMced0uF1gf07VfD8zVMa5S+Sjhk3JSwg3eIkhong6Ix2PYgUxQtdQgtVQzjOF7tmeAQGoKXf7UJyYHLC2HQFyWqn+QFLDsmK0iWPXH68SQ3RCesAion8tkymyhZykIqhcVmbQrPQRnlPZg17IicH96zqbWNkoNGbTQLx/oBZqLmCNfwsC51TWNIhY4JZJ0F03v3Vn5AIi8IG8/k++3En6ux/PyB4+ow8FXgC4nYiWgkNY+/zMGQ+F9A3yM7biGfHW1T7TYzOZ8qW4gG2NPKoJmQEoKtnCXvEh48o/+oNZDPo4iWmAt/MrJmOIHtL4YHI67l6RJft18GZrvjW2V132clhezIqfsCTqEdPeSLDVOXuaH8vH0UROn6afKavIWiefP6V9+vaQLB8R5wf5OtwfsqF4+O3585OZsqPReifoFZASuyVjPL/ZWrqueNt/7/2gH/Ezk8BAXm4dPHWy8F0Mei7d2iq3KQtMKM2aXwsZ/kqLdNSCDfEnhUI56UHjYIaHcAwtF5NruVUl611Sbw/ISAP+Cmt5riGZ0Ff4t4ZK4ZsEMP34gMyjYiN0JUrxukUfRxb72J3bf9c5TPm+ap07sVmNpMuy2qxKQRHO8812CaJrMmqd5GAqO3V35Rs4EpVCa2xJjoAJyY7TDZZdQwHoQrfOjcJt/0HDpsx8+8IZ5ZIH9Q+NV+CvwAnE0hLVVqOFHUZsr8M4iLbeIKc2iaHEVmlHrOQWrwoyCVLsFer3sMeaZ7hl1AzIzuvg/4NgP88BPQ6ArjB2BMoJ9howtFS0qL/ABWeINKJZNG1+g+M2wditzgO8m5ALMeE2pqDcbZI+28fR5QA2W2oAt/cW3c/IFWFnJpPAmzPqoGIO+R73bWyrrqDSiBqIGauYQShmmmBHpWlV15vWyOWC3fxGjaq+Y82n6BWwMRsl1XlQPdhlevYPye/yT0ilK5bnLuKjuLZE6sfpzOf9rW162x1QY2HASkhJRkUb3sKASRE3fOqNyuBS5gJwP+oPzR7bdWLDoD98yNygaDRTc5jK7KAtDYDB17XM/Bz+R/v+uj6/yLeG8lR+H4Uy0rp0VhWYmc7ipPI/2quWJQpcNxM/Mpy+TTvCI08VydoUaPjQo9yUD1IZcVap1PbbMcdkwPLDMUI44E1463oygOfc6MpNVQV8shhEfeOeX3rwg7Tyb2p1eAXI78BvJrJiE0iz3igN2XbzHIQHXZhlkrL4Cbpu+1Pmnr0Ftk7QZcG2GoAvVgRssxWmIykg6KljpjJWU+3XSy3Ba5cM9m9GQ7yBO5UmEo1jA4E5G4HLVcFD4e9EDT8bm0StmXEcbwG9xdp76y8QQZ7oVnjSHCKlIryohPVP77Cx8WrpVkEdDLtxxXNyDSr33hS6q44gNaDtIejp6Wor4yUPtGbhc3fS9cPpV17LN2tcIdS/9NacgYM2ES1EfCMinHwB4zciNxNo5fyrnM9YSr9cKw0IUQxdVijmxXv+IzoKIS0XYlkWDoP0ky5Mm5vSn85Cha/mGC29ldvyx5jMxivyPgVhzZQCFMvQljWeXDO1ZIEWbSj31V/2BEnkfuvb9ObPI2zCDzJ6/DthayuBPCnFPFVtJo3MMmFaTI/7uGA1EbOwWUhvtkml39BzJYs+IZiKhcXx6sTj1BDiTJSpOwY8yFyAOTXYb+l8M6zFkOoXv/pYlNk/nmNVbKmlXBW0zVR1acd5k0SGmtdVUp48inr97wmbd4qkkcQ41wAdJEtzyopF3o2XW/tcWQ8qZ+O1e+sUDiaBVE7cUuVfDZ4Txpqk2zwPKCHcrq/K6/Jb0XNGzp4YVUqE86rFxLsq75zMpXcbAyVRNT+qegxKD5yHi/dgy8KZ3Fq6PqeKeXNPBm1/vhCjKNlSHIU16ALNeBqUr+bzluJBQYp3I5ZSq3EoxgAIHWrP5y2PamUxsDjMgEYoIDENjh2UPyzJ5EMKeoiUhaKl8KmUKeFBNeEex74nEQuUmGKVwhtdpncrui5iBKu3RJRfBNiTp3eTFOweE7n+ZdN4IJF1cye4saxMc3ZBTx+VHajF7fDsU8lj4dYIrm3zzqvIoNGgHtbOJz+irBqaZ8rF4oHXvtc4l9+ZkPlhJuWtCxFnXCF39fN6UoCbCPXRjN+hbXEf8k8VkO9c7Zk88fhva07RazTPcAE4JL5xfLT7E2eheSCuxF0t+sHPOxNR/y9C0Nag9EhMpOIojBwZkCQcYqwT2DnG2F+KzYmliesqkrzfGTpSPjfsqr6xxFam9GJ7mQoBHe17fRk3VTbRgUHdsGdmdiWipjmkS1N5Qt1+S3gMNWXNFAaSne1lVBie7di/JOaLeQqGsaqghgsubWhgBULK0rHWWT4FCVMmppdKIggZBNMpybj2Nrof3xjJHd8eNX+qtFZ+UGowmuY/D21OA7jluMXFb34v4E/0K5j1euhpGRk5r4O7pvoF5bwKxF9lYys08NAt/cRzRewyaldtd7RGHe21kNDVz1vD57IEW0Cbj4tpu2eKJyBsTkFQ3osYErTNev1QifIjcz7MtKgDYSgzbw89jHHK57zNCTicZg18NAqGBNkhZs/RZXWHa3vCK0AUMAEMOgvthd3YhZdtIe0/34vjR+oWRaSb8LqdjLv7xWwrV+p0u8DW2iemw8CtCULCYVQBegR+W5m3UYSCcgq07Yld8l981PHbmBpAy3DQemHDR6r49uMUiOLEl1+sr8GKU1Z8MfHCQADjDOh2sqEJCNDLdTfuZ1MhBEqeaioQfP4OD5kJALraSygRQFDZfIcu398CTQKRb+IKKxv57p9mqT6lmcP6yEJbe6tayIXDNZ5ofzAVT7FoU3HAq3UHt5i6LWL/gEjTRkOau5gdv9cDTlvcZW6Uo8MgagZtkf1bG9y0mPBwLTAzVApMEc6QL+Y+Jwb5XYAp5iGkgdmMVBIq6spuoWJrzOU80rttbq+qansiQyP/rr4FlLOJqhl0eLD2pLlxARWUywcAByYxgrH+SRCOEBmsq6+3D+PssBP/2EnQItTCFogjDZKqtV1VKHdkuWmNuseQ2OOX6RiN4+y3xd3qjwZiRI4nays0iqDm6KnMZPQoCbETbBzS1Vf1LiPNwT0aQhWEypN9tXsdLXWAr1zkNYptj7SY6UaQayZPyOkIjWC1vjGKt9FB499xp+fwebmegtFFB/rNUNb/0uXwy3FKFSnwQ8C2l0p36qmEw+/FU/DAbgCFN/JL7x3VmHo9iN3kcz6jz4TYJj9GjomA9UbUtfS35JwPNnNiyBVLkdNa6HEELpYPMR7WkOOhl0o8MeOFo0WtT4ge+hl5dXlhHmzdqH1NS0HW+EBfpNyu9YGi9CnHjpks2p4kBg6fX3KOrwV7lBIM/04u9EgpTGzycXabpvZyyqWdCaGuDDF7wPlACk36hUVuUEP1KZhdqNMCPHVuJDm83c/3WIsQ+i6LLqawnqQnmi1k7phLzgflMivAYSyaJX4utGCrn+8fW1rt4JiTRPRF2YXYxqbPZQuFAnYlGWZwWF00yrnwfNaMD8en7TiSPlHltmmTSdxlKvpgbPxkJDMnaIZhm1r8aJoyay0LdxxB6VJ2qVkaenmp8EebfitHsGbwwVEXJ4PD+HK8tWtx1fTdMSgk6CpVSyOZZSZa+Q9kJyATgAzLnrz+iuJwuReDqCUBPqAxcIdqG2be9T98FnaGyGnQUezNEntV80b5Ti4X/F0lCDVkka40vRlmLISgMm3peANqS8umOOXE2FmUS/bzAtMFUxnHC5UUKIvcb5mbVLt260YuYC3JMltSe0+Ttt42LsjAnSNb7xBSx2rhYf+hv2sXplS4aEhSp9o/3OAqBOb8z7ltbOY4DnlWOVVe5cQ9Jv3yoItNoGXMkmTv+MJx7CL6P7Ug8hSp0MpFyvr5U4LuI1dId4FSk8Lfg5qY9Eh7yDjQwe3PONuvaQDzYS+OqrKrjOn8/gDDXoK8tVeCz6DqKoC53LFpNxNm5VMHCOKtCIIseLsTKScUI4+WAca+Y/WLY6YJ4efMAFPTagAtEzaxywznaBfObJrA5hUHV6re2Jv9v1ZPPCkRohfFPzLCexLEIRyS5dom+6pwZ4wzeHq0Gr5NQNqMKoQyQfk9bcR91ywFp37Wsia4jS7E63JeMXmmWjxebdBPFu522ELw81jh+rqQlpq9iwnwOyTQrE248hdHbBpEFOs+QVPyIH0+UnsnNPih/Lfmp7IHs4tc4l60M5evRM3FzJl+wZxPhUaPmINUnDCVxg6D6S9jXn5Lo08Sa9gsed4xbq0dbGtRw9GWbx5xOaY0YrDBdepe48vbns88+2YTPQMWJ+oKkgi97EMxI6GJK34aNOynwYmTPketrWvAzwWrcnVoFlKmtbOG2tV4porJZE948na/kafQkOOnAa+VYswHx5cful1tVjsnelx6pSCIZ+HmyMlYDhm+Y+MkMLsf51R8X19KkF41wKTqeH7I4zsC49B+B0a9A+kKsuhVueyuCX8bPrB3rDgpSpejCBI5TFS8YscQhsREB0CyLAOGbw59EIp4MaK8bmiEVxULcsFKJtIWHovbLystT61P2wguBpkyvYdAIqHF2lXtzVux01V/h15lpGqiQDNUGFZ8LYJua3KvJz6NiHe2VMyOKG2S/Uo+x7k+e7DwMS6+s3i78qgRckqXMfuoM5z7/zxqwTAoZblVJT1yh1XtaK1QhIzuWAt83cx0iRFqyffTY7uQivEoV+Gkxnj0sNFsuxpLMVpRFxxVGSq0uYC8zZZx1G0Pg8dnqA7UaIhXm3/U41u5bXGJpkIv3fJx3bqwoweGrrjXmkIYu6TEmWgVxXp+tiB43VD2mb+5f1SsiIe9FJ+gBNvchiuplmzVBS5mMip8+hnOllvEBugkytv1nKyiUb/uiTK7XV5sMR6W98ejFOOmAAAmuTreqfO35ETGfIwx1inIgLlBBAbfTS8RBrlGQ2Y/WSRsgzHplyg5i2nv4LjH1q8xCzpYT4DWBemDRhY2IYsGmItzeFj3FhtgV3I/NRkL2w/hf66ST+b/GJt/kH3SIZbu8H/PgxDI7g6l8vT7wcTtgAKvVTLCnL+RBxTATHM9O6TSk59BlMTCvkBnlPQwXaRyids1KAsao/yOqIRXEn39wICUnexMu+Kddo4PIrV7QJBkzJFXjK1KZys0dfwRUM3ifIBwiicQJtesqnUp074XbH54CN5w5CmM/zSfQxXw12qAW0+jhkPgdbos7n7KSGH2n9/zmuFtkA3SigG96fPAWmZaKyvivcdPHcyOq8Lk72ikTLPYBW4pcD4q9seQZhESVZDdBw6Z59C6ffELCz9acAl+GPldQCBGOimcmxFvusFETwJbS7AdGYdN1BYExZiGidMCWvK3ZzKMEZn5SrJd/QoXqrq5+hzf44OD1HnVZuqjIP9PWseAEeGLqZ0WNyLZupfk5btddkjwDMF9wCaXH7Cy9kKTfCTyQSt7Ri5rdCbc6Onmpa9GBsdA1RvhXOJGUB12YJJyvebBLbe9CyJ4iiwXiZvNg3P7WPw8L8MLjbngNfymuxsL9T+ErVFFgAvL2hY/1tvJR1JGrv635Kj26KbfXN4tqT2LVWF6+DFcQw1bvza8yAY8Gn5CsdybfMIo7Nz9uPNoh0V4xaha5yYTtRvEQsILQJ+Ut5DxfZhj7xi3X3suUc3fnL9Gb0ehVBWNU5k1iGZmU/utVZOzH2IJk85M8WTPqROzwRw5cRZSMRaEcaymGJTFkxyeJhMQgZ2FbJF7cdKO6ngHlmzUMWb1IQNxkwUyhvWmCK3tYRClpRQhCg74MKppe9kAtjssL6U4eWAexdTv2QFqcFtd0PiZduwlMHvQqFz+Rk5DA+wRhZ3zA0VKrNA+7lvrxxCl4PrZ6ow03czVhgpHGbILDOLd4VidTXVVxUiBtBfU33Sr5mFvbFdSvdHugUMjvPAlxHog7ryqNfiSrBPoxK6pPx/ekmsfsBj4SIeG1VfGRJyvBi3NU7WHFUcPQsY3G3UYV4Iz1RoFGap8QfuhcAPqryza2Py6gOzExV83keq23H6nr3o7ypIVtJcARINJafL8QLzXvgObQBIP36Q0rY6Mfc1t2UhuiUUW3vuCmJGHCr4ZwBH/1fvXMfXUZunF0Mrb+6kyQ8o4B1Ga/jABf4ML8XmWZn1Z+6RDYpULGB2Lb/yGa1X/ywy5UboF+Hs5hGgcRjqnUsL5sp8OiQxL7/+bXgLnTb+bbvtBetJu64Ei+R9Jn+/JgWB7bHqA3a3GdA/BzS1fgWC5riaCC+lPB92xkkoo1kd1gwGl28waWreQEXZQePiNbMtrSBD3YYdMnJoy/jZPs+2GepejCgOjBp/9HNtmUneAU0ytP9lypju5ptigS+isqFqx5luMU6jdNqZRSvKB1gskq1+gGzYbUyPIga+tWrHf9pH6Dubs9ajjmOQeGDKgQ0k7TPBD/H2hJ28kaSpYG/7FbwY+mmL6qkRvr9hEJXLE9zAlxKetNUERBKpWJv41V6a/gtoDOUqQ2QnBQggIfrGYEoNpTmX8cBcuwlSVcw6hJhxFh+0niWnTwxZXkPhwjyDGSRLPjGo3R9UNJEJ+BwX6dbyx0/7C2Thl65qwu3e7So6nMO3FcL5ka8HAKiZ+NrCmz4LhzBIz8/k9hzFEAmEUAKT+bNcSCdHQ4BGrysg52R1AyjNJZOiTIVdgG4lgZv9RtqXrbyw0HiwV8zFk5PhgfA9brnwLW2JjstqoQWvAaBu6y80HHFpP8D3jEQVNwCLYt3h3yMocspaPneuR6ZqCRQIiRG6+pu+dyNt2uxzlCc76Cferiw5M9bqVRs1gdOhY+d1g8kAjYgbe2l3PnCdp8FNa0FhNUNua/TgCQyTOM4/z5Vrruv+gy+9t+fmlruUHxR8pgPwo+8hbq7wKLt+8yn1kcGHx1HfTmdpgnpuD+H4NN0XyPJPD3Xt8eJSvaHinRcVWjBW4XRAM4bV6vcgNSZF/6FyDNBX3fdk7s1toWcsOYynXG9lrPe+DYDIMJlIro0jHjbwVQi37x/YR5IrM1+e6lG65JxqrFP19EQMwIUL8VeUKO8gA95UQAah+0FhUemAApEfGTl0y5y8AZzP4dtS1LhTp2Uz1PjQFhtWhU5pNQLLBW1R+15yvR0uP9aheVC8daGUUNom6zBfQbQK/7JR968ob+/zqpYrU2WdiRvi2B0sERiaetKEjwlk8jQgdhzckWNGbDDh/qO1tqcpLFlG3x+DC+MBW9qIpMKVJP2oDL+em7AnaZaIXN0m225sWpvMOBxfk4CU8GxuceLleMn/RTnsYov1bziPWGp9S6jCMSzFkdMQahtt+sDNHhOxS77Kvo9lA6LzgEj+bU1P/7AqnGl0fMSnwdxPGecWUrxD4bYcGNB9jCAufptuRKpDurbopdFkI32zleNT9tuS124BzRtVUzyWD7YGTc21QF4GZACPjw9Au3vxTfA2XhPnlDOMb3AnWUUYsio4RO0o+wRMTutfmmJDUZmzpjfJ5eIPp+LKmGsDzZ6Kh3M9T93rEnERzI+e+RlgK2/W7nH3Hk0TZw2X81F07PMBpHUHH8l/++kpxp+1wQX9KtYA/3gzuZ9qCTs+uihh7/lr6rhB6/BOGcUmce9aHuZHebTxxsoH2MgTNwjNClGy8PdE/4a5+OmjMFX6q8UfJdXfRgkdXi3v9sHxdQcSBxL3s2w86+RgsH8jxR3nTCTi4SwhDidPl++hBhVO+Fj9aNQjpgl7UBWhjnWxHGAAVVQCRxe1pxV0SedmM/VMNR6ULq3za0jtIldmpOHD9TinuHcUQgDuQSRG8WraWz1vKiKJEQb6wIqO3Gz9SFN6+ALBAMKYUj9IszJeC/TPdKGykgXVt+xZYnXLQW3hrr31pN+luM/c9SE720Eu4tZy5cJ2ScBl+L8/mhBK+uIx7MCx46+eYuEJqMlLnN3cpMEQKjavtkjUdlFX9uetQiV6EuNQJDxH25jtpuQ2yZHqWwMZ86XJmvdlVelZjBWIdMpifEB2rdv6LwDa2FaRax7RrCvs1MnISMre71eJ6OH5SShTx9k/rpdN0yd6c4WFaIusEG/P7DBCx6Abr2l1phJlwFNSTkS58s/5gTU3dbVmxR289MSyAcP1I2PxHyCY2lXTtLkzYWbRGB+FqZXtTHGTXH430JAY6iMUMMN7LmbGTKNFgDMTUzdHx/ORM0vlU20/8D77DTuBbCx3NXpSUwx2+UajWCPbuptXEMdJsevAp/TAl0ROwf3stScZlpojjIa241fCZDwBErPGxXPhQK7GAyGjAbDMNNOHKte0VDUCpNha+7zX3DH1sMG5n5s1dUDPexVjEIJ+taSsE8U79d7Ec7PYjKMf3FmlEQHyJCRVQrrtbkZz1iPOafnC0GDz7aB+sLRaKvMREA1/p7ta/VlgHYurICAx70JK601DXezygJLcjzyqCa/a3SbG2MlJZu0XIvwBcQtGBI2urCyGjqvrtlt/KjoNj7+QeOFxCBljzXU9BGBvqNAC5sX1inT0FTXGhsw6n1eICJgoKjD3os2hbrv6b+F2f1+DWcLOz0e7xSs2msD6EEVHoJbe/2yS4LD36DNRl2T/o8wh+Sz4R2JgX/JVnDN776LQ0OMaj0u1GkRNN8OkZeKVbLvViTDdLyVuaAn95zuCPEyWkEa8/UPZpbOAJLx2O8ntGAt0LjXs4ntARgdI9ooaxtXb9rBSKAzzk7bwzUgjoKmhGXOBdXDdyt56bnLTFAHf5jT4uqajSbZB4d80NnrpRePZ4/X0qRm9+xWdJ7RZghEl/hOLlY3fa2dqsuN5TJKerCo10Bz5EBCg2mvjs54ahGhz7tMwPB7LhM6DBbmf2Vt0mLdUOABqCvb1T/alKQZzKoaB++WIRocmpQNcrEJjiu9U2CVUlMfl2GhH5sDbNpmfOZX/aW+JRpSBfbBKBNpMzYnUb7vGwooIx5OamtWDCijyiM1c5AeeSCSGqxNftRAmfPyAtH2n0qZsfJe0s19BmJ4WObmk50LSc6D6wwF4HQ2J0I9myLH0FP+enUBcPGGaMfLL9yBkQy5itPNyE1lvXHS/98DCxpzOjJqGnhGfXf5vyhgkxXR4PUb9jO4NtgRY+Q31wNullHKT8E4XsBh4UHxJ4MH4d7pKWlAN5oZFgY+zh6E10QMUMg3Ev2skar07KOjRy7TC5xp9cahraz8QV3LwL2xrumIkNkhL+sYfZHcW//sEe4McYmmZCwXq2TpsLIGaq+ndBRtTNqSEJtDO9ZwgsBNcdLC38/rbS9OShWQ/6r6YqZraSm0ycvxAV0t3+X3lHfio00WdWHlrE2MS9kMDxBmHCxDC34edibwYxT6kV4iL7KLE+ThfR8s+UZxd+9/WXH8F9DhzY2wvBziOL+zjJR5OICe31W6yjC59ZXCjvHn/2EPcMUrMxnSgJw/+xrzYU4xyscHHu8iofbfUhc7tYN4j79Lg3STxjrVDhsLm4WT9Wj+jlJCkAaWxxeukzDaNTFFWJj4bKNxqHM9vlZhXNmWlAV5/4tmxohGCdNmzVMVd6euTkEABNcz4i+rDYnixYkTprPPcUmLX5T0GbC7LMCIp/2MT3V56LA/oCu56GFh8m90yhPmd3y/RktcGkiVSYCZWfeKia8LRHpoF8Q8QAUXFFZMirn9zMMd2VZS6ShyWavrpRnggWvkmR45jXCMLRklKM7Ax9BEbLASJ8DUo+7PSJ+RVaQEoJjGx3VQNTHRdXMHft7GTORQRmbcq4qx/JrN0ZQqYZ767j/c6254oA4lbUXEKAeThuFIYy7HK+3SFdGX8R2Jiq/7b2Z6YBmFGPDEfDWE7IIFjp8JWURD8qu17qVxB+gl657K6cn4lBvToD+bSGMjFLbN1iRReb2CNScYmilJVi4zGCXdzS4z6+RpxNRUx3WC0xiZ51SQ1r2aXuS+u/mv8wwCa+4IjeNCuklCdCZ40s3VNej3cmVML4Ph4wQfnU9bb/l9OcZoz5x3CPwdRWOONEzMPSdIacY2nkY9oPrJS5i4w7D6m82EJX7WMRyTUSmLYdrTQHHU9++qBNtxFuPCwAD0wmZcUQTePZLs41CWCGGW8gmHV0MJyZhapv1XbMCmR8rxgcFWBrmf+MVUkvlO5YI/dgYW9dRnnA5OCd7Q21Nb2PZtNt686hkxNL7mcWuZ3ZNynDVQFeBp/yFKYpbKs18c0enPgFR5zXcUpMX8UNG9U2LwI9a3EoIf8htmtKZiDoe9PBkEpWmn9YjD+qzc2hKDcrpyYOlXlQHqatC3LSx4ynM1ZgMfkep2XQ6U+fcPzcrq9oD2dg1cBBVXFfgwy4SgxtoMj/3ibfKA1bRncYfujf1LYCNl1i3HU+Ae7rYaqy1+niQupvKk7y2l07JCwZtWbxDyn+QdJtomdqtV/yyRqculTsuheLHwjZ+COKbDXVhZ3r9/kKdi8BDOgBUZoAtIMT+3IZij8qJIVeejBlCuqRZ4H5EhGMYOm5xliI7JyAXteFtOZ3kk1WBQMHV7lDZTtc2WGjJN2ShDCt7XEQDWHcfN0ki8+fZwBqPAZ/1d15Xg5FrUgudZAGsJfA39Zhg/D9Vq13KGsXPKmA7ihJ2e5F0pkY4jYo3LP2bowl/Y8xzTi+2P5too/18tsx61bL8KPlb1lGGFSq9LRblHNN5o7dPEQAzwNewMWMef0z/3iphkAAms2/I+bh85h8Hhfx8Iz1bwZIjhust+pQctNwjMUfAFcb1AbcUP5J1Nx1jufLCyx8KHvIv5qaA/WROV55xtCJ5x93yPCWlD9ZNMDwm1x0z6C2EIhidY7GqWGIU16TEKXejbniA17o2oSWOEqGvD8mMvR4NrMCMj0OqIz+0I4CJXlhKx2dzhLgMnUAbPmEXtVznt/lPWLYjW6e0HGEjY81byB+jk4KVJXD2AQazh6G3sNysw3L6MvpGnPthO04msRUb8KY+og5ju8c3F/q/XAbEf4HVQZKlfg1VC8faCPfm5Ks4jC2QTMAv5FfP1/IRbvCcdaYvVrP1Q0OPGQsi/ZyxeBZ/Hyq9vVAN/HS7HJXQfqWrOhGCwWR7+48OQlPUSO+g+6HuZSPdYLeespOtIZrA4SyLBkMZ7FkTryi2pMKAd6UEdMjNzv06TXo2+ecOGjqCSBov0hLGA1owyoZk9ZDMYzOThn330EpTCjaacjjuzuKf2SXOJi6kM1AWMTc/ZuKNReBGmqy4bbdFovBwXjXx/0SC+aOTVfN6BnLKqJX4Y8wq5nyJfCy+gQljMxGM/+K/oGPkC7vqOEG7sY4zEd8wBwzW9SkaTaDe8vB77DGfEhGXsGAY0c7v/km3EqbYWMo7ElhVCsFE7fOuN684FCOo4ieXYh24Sq0KQzrNjyEKTuLlLFix1lu1HsprXyORTJ9xDjwa+So8kha4De6LeeGE4ihClsjVqdZxI5pQjeAgUgB+sTslzdaiZHq7rpc80YwfHc0HnCyA3NMBYF8u1F5G8rsr6Ul3snePpL23fBLLcgHCZ9UNzp8udDdzVm+fF+S9XgjpCctM9nqqNkPWbkrtudp1OrE3VCmrh/m1ImPVq++Exb04pizdtsLJ2VMI1iDbAbuGE3+xrgWW0B2Lnk3WCauKenE92QIODxmGpuD8triD0pUeB+r68FLHv7pygExSOCaY3+rMa2AIByfkATKbLTuRQNDESfYMBJqFTNWUtfZR+VXZnA6YaG4mHZ/SJLIT3MHvFAQIQtb/yIZf0C0ny2AQ6ovdZZ+TCk4t0+1kEtgcioCBI1HIgA6olFf0p2I5/d+zxByP4pVfh48q+Ej276U+Sl/v5tN9Pz7DA/0HW5Zh7r1/P0yCk1ngdLOKsJGjlLcJTuXRr6O58/Ungeqz6zZa0rssmp4kJ03Tr0Y2AvGNt0PqlhSNxArvRevqyWF16mJsgEiuCk6/IBpdaVgtw8/j0MjUxZbscMSxcIEkbUHg3pS6AnD/i0R2Rn2TfpF/Yew0o6AnjXJ2Yci7inSLU66jmXzwWdEGPJE+HamwbsLKUaAGKpqry0TV4h0fMMjxAIocQ/CqzgL/JXZP66FKkQX18n7WZUuS2+p3d4gDfBPDUXHafUaudZp2vRSmox/yTvs+VGYmIk3gmflkwaQUpxin0bIINILOTlYfadQ0d5f+79eVgF4br/9kzsPyXkLDpl30p3kj+BKEG+td0n4A5eW+12dzXoIEz5nZKMqTSykOickA4gyDiEqHclSwOHNJbcpiMLryuX5DNc/paYQ5amtlZ13hgIVzGSwVcCH62ZpavpIyUD8C4tk1BKLFqCMyTxtp3VuDaBCG1C/tEf3JzOoHg19WD6p/0jYQfoDUTIsLvViOj3qoJ0QcL5DpLDB5gb090PR4ROSMNU6smlbrt8dDlzW/uy08hLXkpsR0im0mdhXgpQMiE7X5IsewQa4VytEr57busadBbiVLxnGDDVoJ5p3zSwXe/d9L1m6uJyqN1DQLgStu2LqCvZtwQyxRA+b41JReoKmvTOq4u819DCtZsX1mgjDKki21X06Ac2XzdVXok3cIwNWnTfvuvwi9SeSpTwasIPQ2S1lW0uxATr0IT5x2LNPym+ki7xLBrHNqEswJWnxAsork/3B89HSjh+s8gTZaSveS1oaqW87NxyFK6hRIP7ZprQ4rojnlcMoG8wZLE0t8OA4KhdXP8/MOV4JEE/fkDQzfec8cBtn1twA5KxF4Af02cXvBrCSsFsWel/U2Xa50XNoSCCdeEHHME0slS67AyWZ3N/GguHXcAhNDi8yk6Vi6UiOzVTnVTcbbowigSuyhMvj1snMOSBlzn9Fa32nLGr5A0GBuyDr7q49iRgjl0fN0jSDrJFHQeMMsTq1J+nOPltY8kjhXyqqaIIa+pzaBv5UYjYLIPxWwMa/BbSY4nGjbWUtVyvv+IPME5PgxOY9KCTr487vODY0ymfxFBEQnN2mEu1Hpfwb3x6CfRpHUw2di9Q9Ms3QTsoW2J5o4cp3KbDycNlSmAOgeQYx4AeeNi2fPHnPBm0CC+9qDifzPsF1qFYaKLAfrK7DHD2qn7wj6qAdar9hsBrZ2vcSYXt/G78X78go1iGzQYRWf3a5GAB8dQJUM3jy7m/4T0WJ7EKlBCZGADmUUYW9d+Zb/rhMSPBi63TjFQtgIkwYhzx0C+MiFKLiiBOuFuKcCdS6aNNVTcpgRbMLI6Q9aL9RbErpMiVwNKaZoN4LJ1tuHdQQ55HizhBIpjRGKaF4CI/Z25W6sf96Uv3vbBS8NB74TLti0D8DuxadmDQG5Nc1Vkkr4lCafIu5qwuCK9ANKNuUJBc7pMVQYdrtAAcSFeJVtd5ViYbZschHRSX0NVUEcsOnsjRvcNFcveEb1G5pqGh/pC+M7VuuYY5F+/1uRI8DyPz0iqD3xfvkLgoH/qaI/8IpfYOmCZxTXPpe1C0cCmYvlPm57tFfLSEpD4VQufJ24OWg6vYC8CI3kyrbPWjFg+Wm71XowAz6X8GjCW1HrfQdc190pjw8jcb738SRTmnLXBad5P+p2CntRpIrp+YExFrR9+3z5b4SKSHuE9PpcvJKZ2dyBnP2wNVcTwc7lR8bIEJNZOxKNOUHmBUlcx1lXy4d64yiYCXW6Z6l1a5qU61XVhBdNj6v4QYZr+gZcekuJQDjQmQ6/J61mHPZ3eD6b3onhe2vBAspiKbXsSfCN9At0oAxYkC06OxwjmTF0NSsYRvdYwh+41a/sqUpCaaxPXWCbrcTg1VqL4cggzZY1CZL9ouTaD2N+zTzHFX7jdr741fLc0KX8j+NNo1JH7Yhp58rTMosFgkTHR0soKygKbv0wIaPpFUwOq9lGeQYky0aIjeYLAe3rr8WyZRk4PAs7M0+lbki59SuSEZYlBpqsETVknA7dpes4m9K2btmzQkYIZihpAtwoulKrdszfS2RzqMa/M63N6Hb27s5Y3ulGpgAChMKTUbTg1xfNAVpcMaoFUO/9hHWiGM55Pzz/PCzm8eOWVFMzZR9mkf9ysmHwTSR6MD8TBqYHESzTdAyj+EPx+wd7KYbdW1QKUoN37DHFsf+clIp/qYEEMJjnYyqsnvZYAc7kUOYApUPG55oY9zULRMI3/Mz7Wo13T+3ia+FJpakIQkD/etvldMMrINRrTEAONMhE1fT3fu3zWwOnEC6gky4fgwlobiI6o5A3aOEs1E8O/rFCxnVZgz/0dYiZwXZM7I9JUCnRy5RvKG13Cyb1XJzY1BV8Uj4f010xoBbp+/qizxxr1Sn6X1jIglRsi5xhwe/IT13Bw6eokLmjUz5J3HVcEDjw+Lz4Jxe7rkaOQZIbO0NRsB7+zjciPU/wyZWShZu2qtquEkxurFzhggHaP5sraUGbwEt7QA78mm51znHJ1ld2WZ87NdA326xXxKoPdnpxU3N/NWpCFHfzOAieNxXWHxywBUmlt93sqlaqKt4nhHbVtyneZO6cBMcNWkexpgKjrPkcSwi90cwL0VT9w1mlgB2JlB4xQKNL2fsfDjLRs9sWYsU0NEQNpM1X47kxrbwVTr4xh7MyW2KCCiYOFz9nGTsv8BCoikKlfZpDuG9w0NbWYzudfP/kzNa5jTPl4/7QVfsx1Fx9OLU+lKWdrOHGy94Rb35W/Z04+/6xmPamCxb3euDJ+/bSGEwxqGphUyZPGMAIsyyzAoXCyv5vSbcBkHkPncb+0fTrcJHSyjoxMhWgfHIcVw9IXVFyo+WZsziF1t1dMcurPFYchsLrKNZorCuVGoXtrZF0lUkpeKVXVtghNwRCcIacezlXn7flznl+bOl1GlK0MFQEmMt0Zqpd6EQl7ZArzrs2yLEh66nBCDnhYLbqtSiuoA+9yHvIxRHchpg2EKEt9jV5NaPQ1nXJt2efv/d+koxX3V4+SgxyHE1ep+tEOZP9ji4iwqfAuYEUp31vI+krZw6nNbznSYJJSlNpqi6lAdo57r5N66NtJ/MdaPhA2K7RhMoo/Bo7tqtINrKN1lP2Ln/xVWUHtVLVm7zxZs8f/aiXZ0ISa1pY7wUIPqq/kcyqnbfqZ3f3EHCiELAUo+3DCdMLG3x5usWevZut+ehAHTreKUj0A4A3c/Db32wI//eFaZn250BVKAjHGZ06G0FVGD2Jcvo9XZ7oPf/WIYOYnv5j+x4kf5q1g0PqR44ZRne7oRaHbKifha0AyWxoY7L0pGwCYtCyvGa0mX/fh9EuCADaHasAhtpUmR1TKLIhiwpCAUzvuor+lD0Ga6RFVPm2hubXNExwrzw2I++d77mrNYeMdJtwl1IPVmlH2OSiOve/Ls/5j8zfYboDzNXE+XCht4mLHuM3YKrzQD5nOO1N+l5XsihJhkVs94JUuGqQ3Kk7V2KhzmiILFoggr/UnAaQcRrTBxbUal/i+yVySid/m0xlo4Lc6osltPIvy1yVc14MFHgYbOXyXkLH4Gf1wRC/G6oK3fQ/BRSajSwgeB1d+RsaCBLtLuraS2E2GRtvY/snja316tzUcq2z5NB/y6qLi3gWeep1xbf7E4fsHRT7okXpH3+p58c22J+E4nFwLszboVqKkVHjDUSIeUPfuafkcvooqJmNwJJ+qOaG3itbjMJXXTow97GAq9rNLkZ5/aSaMwMfIQ8MnBGv06ghiAhc6na2szncJAS2ZTNINw/TyZIwTlm7dXqGBgPwOXy60mLgbOKwL+KoshNtz+hCNDdTvPstOO4w/sDvnb+/Teb8G5FCJHr2KImi3I6+ZJVaIPvBEMTrixC6ARSw8/unw+iO+j6ynv9vsDF+0VLJ4fK8e1jHNmwMREIPYVWacfXtDcvifR69yfzOVF4PhyvYdSXVJ9cLwOhK9YpLd1rWhNyYE+Dd+mr2tps1rqA5UwKVPNlI4xwZEoWrjQTysgQM9D0Pu+qRaCl19m1ljSUCkt79XLdI2XUxMGaeCpAAYb4spIjOe/8+MmG05cJgcAoXxe5zRk8s+1coo2ERpjuAw2wQPXim4TLtd7Y4NtAAnMfVVVhfV+mDk0/gRUR1UN8ariHMN1VWQR6fpGhfCQkon4rMYv2/Ym+QckB8b6ME2ZQPINhgP4pWuLqFzBvPQQXzTfPchASsLy0gftaB+Ps8VmNWKjo5QOMipcsAbyeo92mvtrPTRGnfvstQwP9k6L4cPVNht2VY0J3Fr38TjUekWc+pw+pv9FSDY2ur5nAvQpoh9Yv2kojC89RwIHr+Ui4RMcxp8B27OLDhHLtg6hvMe2qwMsVzs1U2YEgTi+O9xknqECSfVJaQfXcHxtNQ/51EG553EMYbI31X5DWpIIjhovfaH5D2R8F48LZobxq19zR+Ein9Hg3Gnsw2xaPPeV+WUqwzhxjUqiyHlICOi8Fyu9I0LFlu9LXGBcyCBxMAjzH0wMlpzEYSjgxj98AdvDn8w0RP1O6O8E2Bh0qnH5lrphYitYh34SPmrsOjV3DJNS9H/OuvhBOXYAcHsHW4JMmgRLRY0pig8C2y7ZgtS87LwNTu7FGvYB8Dp/Vo1V7q4vR/a6Qk2sDMkOEHtBb0JRdQ6qjxigpaIAmmh5cbnkpYBjRpnOhLaGj6CKofzeHmOwQ9aCjfMckQ5uh4BZ6l8xmc49k01HQnB49KDGtT95Sm9Yx1FtEqFr0tsoKQ0SSwM4NWaFAbZ4zatAZKT+EflVU/xsCJhORdFN9XXtfYU3JsXBWjTRTSpaNPo14ESlopDJz6FyncH03HPDuE7/vqxQ/J/Q9ZnBo8oepJCFYyQpwBzc+vBUGxxRwI4yL184qCc6Bnspt54nAAm2RVFWJ4zzGPxEMoqy0gAH7DJXgpFq5I85Fj4kXiqVuFz20fDMn6YkTKZYRtqTNe/oSc0L2SzwBmtZorY7w7bRKj+mt9PWYwer0daEkuY9OdPhdte4YjYEPB0Vf8ErCrRquMz4wf/SZv9w0Gfek/s0R5TW/ELnewLQb81MO2mRb+r7lPWJBBkLjyVjQx6n1rWz3NVuaGCNvmeNnBhKAMQ27m4d5DkaNC7KohkIXss0gwyzWAEstYwqcLsutMTyPccnLFiowdldkFumHkAj5UV2EWM2dIbZwSdgnmwIiy/gZfywFZ16S5qjkFVfJweOGq3xrRCr1bbAk7pakaP/gxvp3teh+RPcXLCVEw3Ks3rR/3zY9CDsZvmcttgQGtWTxEuEbBu9XSphZyO4QQw3/bUxHbxsIO70UNc8FOJruteyM3WDyqc2t8133OhoYN208AJPjhoznHE6tkBwnkZFiC6NX3caZbPWHT6DRTmvJS0gTRiPK9mRS7UXl49J3hSN5CW2KTgA7e0TYJQLsSw+AygGBZzA6cjXNwc4znJANM3QBQjb7WXDAGRcr/yz4YQ+HLW8NjGGRoDAiNomkV9sGHRutOlpxoExfCE7YdzFFXYwvLjTFimxbBBOP7uw24LbIeL3cGdR6jX3/zkTk1AsBzVtPSpwE80It0M7lMay2u+pn2TVVpsCm0EjYAlik2wn3knh734DBmyvE7M91KjjQXXwj5rt0S/FVzgHDRBjgIoXv+/D+bJzUhUL4+Gk/fdyDbSQJPTmW9SFs96DvNlgOF7RCoDGjYbG3Zxol1jNuYhHaDrmlFQEhjpiWldLyqQevEF7SkCdlzjJyREl0Ze5XHABUs7FNMX1O0aBKckIYqpT8x0Bcjsuhkd++/3N5dgGqWBddT6vC4u3f568DlKcZnmO9PMBbjL0LM7OEMxah9BFoQzbkBO44P7kX9Wqenyag07Xb6tNX/D1PQPRRlSRWcw9DE7KGT8/rJUmE9WZq3NdahX/27o832PtGS0n1/DTGr0KCBT9ceSSFQKmfkZadnMcS+zwbN6x9AkxjoLxXBFXRxUc7P4oj8ff2P5F54lac0c65pBdQ3ElNc9uBY/nLLUU4hNQIxxF/7yL/XekwMO2Gj23otS3RB+bb9oWQuXGHL8eZ4n+OOGnoQ9/MoeJmvzUir182zpVOBB6TkwPRB3xeiiAvorHPLNpLRjXTANdyJNFv8cG/zAIhJ5VBnw1Yk8+CjUi46zpHw8EAA36H/UwqmPtor73r0NtIEXmsGR188ArTee6mupFvuHyNf5gKvcWKRqvi8wPzkkbAmvX64ORE/DMEDYz287JI7U4BXXv0gBEsXg9etxXSZ/eZzUASlMhbwxTgPfjfwUjVMjUe7Jfxn5a6jScQ0ntwFbl8uiKIL9C5Ez39yZRvYtoaZwOP0ulB0FW1kGXsqYNv6JjzKz9nxxPRYtcg+B6SUi+Ol1FUKxydhp9ehJcx+CtXEXTbiiaLihYb5XHZQi+jik2SrZVIPR/wjGQCafYuC0u/+qEHPKgLUeUCBYx9AEea7YOOHFAcRsB8lhuKjCEl8vczwWDKr6s3PlubmhsVOMdjHlvL2XWaFhGsPPepZP742SloRLBEL2AbjWcEgcWZWAtcW/OIKs3jhC2rVWRy/AxL1jl3g3mx9H3wLY+97Y18CgaHIUwb8fsvizJjqPw==\"}" +} \ No newline at end of file diff --git a/backend/src/db/api/analytics.js b/backend/src/db/api/analytics.js deleted file mode 100644 index c0e503d..0000000 --- a/backend/src/db/api/analytics.js +++ /dev/null @@ -1,366 +0,0 @@ -const db = require('../models'); -const FileDBApi = require('./file'); -const crypto = require('crypto'); -const Utils = require('../utils'); - -const Sequelize = db.Sequelize; -const Op = Sequelize.Op; - -module.exports = class AnalyticsDBApi { - static async create(data, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; - - const analytics = await db.analytics.create( - { - id: data.id || undefined, - - student_engagement: data.student_engagement || null, - completion_rate: data.completion_rate || null, - instructor_performance: data.instructor_performance || null, - importHash: data.importHash || null, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { transaction }, - ); - - await analytics.setCourse(data.course || null, { - transaction, - }); - - return analytics; - } - - static async bulkImport(data, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; - - // Prepare data - wrapping individual data transformations in a map() method - const analyticsData = data.map((item, index) => ({ - id: item.id || undefined, - - student_engagement: item.student_engagement || null, - completion_rate: item.completion_rate || null, - instructor_performance: item.instructor_performance || null, - importHash: item.importHash || null, - createdById: currentUser.id, - updatedById: currentUser.id, - createdAt: new Date(Date.now() + index * 1000), - })); - - // Bulk create items - const analytics = await db.analytics.bulkCreate(analyticsData, { - transaction, - }); - - // For each item created, replace relation files - - return analytics; - } - - static async update(id, data, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; - - const analytics = await db.analytics.findByPk(id, {}, { transaction }); - - const updatePayload = {}; - - if (data.student_engagement !== undefined) - updatePayload.student_engagement = data.student_engagement; - - if (data.completion_rate !== undefined) - updatePayload.completion_rate = data.completion_rate; - - if (data.instructor_performance !== undefined) - updatePayload.instructor_performance = data.instructor_performance; - - updatePayload.updatedById = currentUser.id; - - await analytics.update(updatePayload, { transaction }); - - if (data.course !== undefined) { - await analytics.setCourse( - data.course, - - { transaction }, - ); - } - - return analytics; - } - - static async deleteByIds(ids, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; - - const analytics = await db.analytics.findAll({ - where: { - id: { - [Op.in]: ids, - }, - }, - transaction, - }); - - await db.sequelize.transaction(async (transaction) => { - for (const record of analytics) { - await record.update({ deletedBy: currentUser.id }, { transaction }); - } - for (const record of analytics) { - await record.destroy({ transaction }); - } - }); - - return analytics; - } - - static async remove(id, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; - - const analytics = await db.analytics.findByPk(id, options); - - await analytics.update( - { - deletedBy: currentUser.id, - }, - { - transaction, - }, - ); - - await analytics.destroy({ - transaction, - }); - - return analytics; - } - - static async findBy(where, options) { - const transaction = (options && options.transaction) || undefined; - - const analytics = await db.analytics.findOne({ where }, { transaction }); - - if (!analytics) { - return analytics; - } - - const output = analytics.get({ plain: true }); - - output.course = await analytics.getCourse({ - transaction, - }); - - return output; - } - - static async findAll(filter, options) { - const limit = filter.limit || 0; - let offset = 0; - let where = {}; - const currentPage = +filter.page; - - offset = currentPage * limit; - - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - - let include = [ - { - model: db.courses, - as: 'course', - - where: filter.course - ? { - [Op.or]: [ - { - id: { - [Op.in]: filter.course - .split('|') - .map((term) => Utils.uuid(term)), - }, - }, - { - title: { - [Op.or]: filter.course - .split('|') - .map((term) => ({ [Op.iLike]: `%${term}%` })), - }, - }, - ], - } - : {}, - }, - ]; - - if (filter) { - if (filter.id) { - where = { - ...where, - ['id']: Utils.uuid(filter.id), - }; - } - - if (filter.student_engagementRange) { - const [start, end] = filter.student_engagementRange; - - if (start !== undefined && start !== null && start !== '') { - where = { - ...where, - student_engagement: { - ...where.student_engagement, - [Op.gte]: start, - }, - }; - } - - if (end !== undefined && end !== null && end !== '') { - where = { - ...where, - student_engagement: { - ...where.student_engagement, - [Op.lte]: end, - }, - }; - } - } - - if (filter.completion_rateRange) { - const [start, end] = filter.completion_rateRange; - - if (start !== undefined && start !== null && start !== '') { - where = { - ...where, - completion_rate: { - ...where.completion_rate, - [Op.gte]: start, - }, - }; - } - - if (end !== undefined && end !== null && end !== '') { - where = { - ...where, - completion_rate: { - ...where.completion_rate, - [Op.lte]: end, - }, - }; - } - } - - if (filter.instructor_performanceRange) { - const [start, end] = filter.instructor_performanceRange; - - if (start !== undefined && start !== null && start !== '') { - where = { - ...where, - instructor_performance: { - ...where.instructor_performance, - [Op.gte]: start, - }, - }; - } - - if (end !== undefined && end !== null && end !== '') { - where = { - ...where, - instructor_performance: { - ...where.instructor_performance, - [Op.lte]: end, - }, - }; - } - } - - if (filter.active !== undefined) { - where = { - ...where, - active: filter.active === true || filter.active === 'true', - }; - } - - if (filter.createdAtRange) { - const [start, end] = filter.createdAtRange; - - if (start !== undefined && start !== null && start !== '') { - where = { - ...where, - ['createdAt']: { - ...where.createdAt, - [Op.gte]: start, - }, - }; - } - - if (end !== undefined && end !== null && end !== '') { - where = { - ...where, - ['createdAt']: { - ...where.createdAt, - [Op.lte]: end, - }, - }; - } - } - } - - const queryOptions = { - where, - include, - distinct: true, - order: - filter.field && filter.sort - ? [[filter.field, filter.sort]] - : [['createdAt', 'desc']], - transaction: options?.transaction, - logging: console.log, - }; - - if (!options?.countOnly) { - queryOptions.limit = limit ? Number(limit) : undefined; - queryOptions.offset = offset ? Number(offset) : undefined; - } - - try { - const { rows, count } = await db.analytics.findAndCountAll(queryOptions); - - return { - rows: options?.countOnly ? [] : rows, - count: count, - }; - } catch (error) { - console.error('Error executing query:', error); - throw error; - } - } - - static async findAllAutocomplete(query, limit, offset) { - let where = {}; - - if (query) { - where = { - [Op.or]: [ - { ['id']: Utils.uuid(query) }, - Utils.ilike('analytics', 'course', query), - ], - }; - } - - const records = await db.analytics.findAll({ - attributes: ['id', 'course'], - where, - limit: limit ? Number(limit) : undefined, - offset: offset ? Number(offset) : undefined, - orderBy: [['course', 'ASC']], - }); - - return records.map((record) => ({ - id: record.id, - label: record.course, - })); - } -}; diff --git a/backend/src/db/api/instructors.js b/backend/src/db/api/course.js similarity index 54% rename from backend/src/db/api/instructors.js rename to backend/src/db/api/course.js index 8d274f4..9e38519 100644 --- a/backend/src/db/api/instructors.js +++ b/backend/src/db/api/course.js @@ -6,16 +6,15 @@ const Utils = require('../utils'); const Sequelize = db.Sequelize; const Op = Sequelize.Op; -module.exports = class InstructorsDBApi { +module.exports = class CourseDBApi { static async create(data, options) { const currentUser = (options && options.currentUser) || { id: null }; const transaction = (options && options.transaction) || undefined; - const instructors = await db.instructors.create( + const course = await db.course.create( { id: data.id || undefined, - qualifications: data.qualifications || null, importHash: data.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -23,15 +22,7 @@ module.exports = class InstructorsDBApi { { transaction }, ); - await instructors.setUser(data.user || null, { - transaction, - }); - - await instructors.setCourses(data.courses || [], { - transaction, - }); - - return instructors; + return course; } static async bulkImport(data, options) { @@ -39,10 +30,9 @@ module.exports = class InstructorsDBApi { const transaction = (options && options.transaction) || undefined; // Prepare data - wrapping individual data transformations in a map() method - const instructorsData = data.map((item, index) => ({ + const courseData = data.map((item, index) => ({ id: item.id || undefined, - qualifications: item.qualifications || null, importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -50,50 +40,33 @@ module.exports = class InstructorsDBApi { })); // Bulk create items - const instructors = await db.instructors.bulkCreate(instructorsData, { - transaction, - }); + const course = await db.course.bulkCreate(courseData, { transaction }); // For each item created, replace relation files - return instructors; + return course; } static async update(id, data, options) { const currentUser = (options && options.currentUser) || { id: null }; const transaction = (options && options.transaction) || undefined; - const instructors = await db.instructors.findByPk(id, {}, { transaction }); + const course = await db.course.findByPk(id, {}, { transaction }); const updatePayload = {}; - if (data.qualifications !== undefined) - updatePayload.qualifications = data.qualifications; - updatePayload.updatedById = currentUser.id; - await instructors.update(updatePayload, { transaction }); + await course.update(updatePayload, { transaction }); - if (data.user !== undefined) { - await instructors.setUser( - data.user, - - { transaction }, - ); - } - - if (data.courses !== undefined) { - await instructors.setCourses(data.courses, { transaction }); - } - - return instructors; + return course; } static async deleteByIds(ids, options) { const currentUser = (options && options.currentUser) || { id: null }; const transaction = (options && options.transaction) || undefined; - const instructors = await db.instructors.findAll({ + const course = await db.course.findAll({ where: { id: { [Op.in]: ids, @@ -103,24 +76,24 @@ module.exports = class InstructorsDBApi { }); await db.sequelize.transaction(async (transaction) => { - for (const record of instructors) { + for (const record of course) { await record.update({ deletedBy: currentUser.id }, { transaction }); } - for (const record of instructors) { + for (const record of course) { await record.destroy({ transaction }); } }); - return instructors; + return course; } static async remove(id, options) { const currentUser = (options && options.currentUser) || { id: null }; const transaction = (options && options.transaction) || undefined; - const instructors = await db.instructors.findByPk(id, options); + const course = await db.course.findByPk(id, options); - await instructors.update( + await course.update( { deletedBy: currentUser.id, }, @@ -129,34 +102,23 @@ module.exports = class InstructorsDBApi { }, ); - await instructors.destroy({ + await course.destroy({ transaction, }); - return instructors; + return course; } static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; - const instructors = await db.instructors.findOne( - { where }, - { transaction }, - ); + const course = await db.course.findOne({ where }, { transaction }); - if (!instructors) { - return instructors; + if (!course) { + return course; } - const output = instructors.get({ plain: true }); - - output.user = await instructors.getUser({ - transaction, - }); - - output.courses = await instructors.getCourses({ - transaction, - }); + const output = course.get({ plain: true }); return output; } @@ -173,39 +135,7 @@ module.exports = class InstructorsDBApi { const transaction = (options && options.transaction) || undefined; - let include = [ - { - model: db.users, - as: 'user', - - where: filter.user - ? { - [Op.or]: [ - { - id: { - [Op.in]: filter.user - .split('|') - .map((term) => Utils.uuid(term)), - }, - }, - { - firstName: { - [Op.or]: filter.user - .split('|') - .map((term) => ({ [Op.iLike]: `%${term}%` })), - }, - }, - ], - } - : {}, - }, - - { - model: db.courses, - as: 'courses', - required: false, - }, - ]; + let include = []; if (filter) { if (filter.id) { @@ -215,17 +145,6 @@ module.exports = class InstructorsDBApi { }; } - if (filter.qualifications) { - where = { - ...where, - [Op.and]: Utils.ilike( - 'instructors', - 'qualifications', - filter.qualifications, - ), - }; - } - if (filter.active !== undefined) { where = { ...where, @@ -233,38 +152,6 @@ module.exports = class InstructorsDBApi { }; } - if (filter.courses) { - const searchTerms = filter.courses.split('|'); - - include = [ - { - model: db.courses, - as: 'courses_filter', - required: searchTerms.length > 0, - where: - searchTerms.length > 0 - ? { - [Op.or]: [ - { - id: { - [Op.in]: searchTerms.map((term) => Utils.uuid(term)), - }, - }, - { - title: { - [Op.or]: searchTerms.map((term) => ({ - [Op.iLike]: `%${term}%`, - })), - }, - }, - ], - } - : undefined, - }, - ...include, - ]; - } - if (filter.createdAtRange) { const [start, end] = filter.createdAtRange; @@ -308,9 +195,7 @@ module.exports = class InstructorsDBApi { } try { - const { rows, count } = await db.instructors.findAndCountAll( - queryOptions, - ); + const { rows, count } = await db.course.findAndCountAll(queryOptions); return { rows: options?.countOnly ? [] : rows, @@ -329,22 +214,22 @@ module.exports = class InstructorsDBApi { where = { [Op.or]: [ { ['id']: Utils.uuid(query) }, - Utils.ilike('instructors', 'user', query), + Utils.ilike('course', 'id', query), ], }; } - const records = await db.instructors.findAll({ - attributes: ['id', 'user'], + const records = await db.course.findAll({ + attributes: ['id', 'id'], where, limit: limit ? Number(limit) : undefined, offset: offset ? Number(offset) : undefined, - orderBy: [['user', 'ASC']], + orderBy: [['id', 'ASC']], }); return records.map((record) => ({ id: record.id, - label: record.user, + label: record.id, })); } }; diff --git a/backend/src/db/api/courses.js b/backend/src/db/api/courses.js deleted file mode 100644 index 318f68f..0000000 --- a/backend/src/db/api/courses.js +++ /dev/null @@ -1,428 +0,0 @@ -const db = require('../models'); -const FileDBApi = require('./file'); -const crypto = require('crypto'); -const Utils = require('../utils'); - -const Sequelize = db.Sequelize; -const Op = Sequelize.Op; - -module.exports = class CoursesDBApi { - static async create(data, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; - - const courses = await db.courses.create( - { - id: data.id || undefined, - - title: data.title || null, - description: data.description || null, - importHash: data.importHash || null, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { transaction }, - ); - - await courses.setInstructors(data.instructors || [], { - transaction, - }); - - await courses.setStudents(data.students || [], { - transaction, - }); - - await courses.setDiscussion_boards(data.discussion_boards || [], { - transaction, - }); - - return courses; - } - - static async bulkImport(data, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; - - // Prepare data - wrapping individual data transformations in a map() method - const coursesData = data.map((item, index) => ({ - id: item.id || undefined, - - title: item.title || null, - description: item.description || null, - importHash: item.importHash || null, - createdById: currentUser.id, - updatedById: currentUser.id, - createdAt: new Date(Date.now() + index * 1000), - })); - - // Bulk create items - const courses = await db.courses.bulkCreate(coursesData, { transaction }); - - // For each item created, replace relation files - - return courses; - } - - static async update(id, data, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; - - const courses = await db.courses.findByPk(id, {}, { transaction }); - - const updatePayload = {}; - - if (data.title !== undefined) updatePayload.title = data.title; - - if (data.description !== undefined) - updatePayload.description = data.description; - - updatePayload.updatedById = currentUser.id; - - await courses.update(updatePayload, { transaction }); - - if (data.instructors !== undefined) { - await courses.setInstructors(data.instructors, { transaction }); - } - - if (data.students !== undefined) { - await courses.setStudents(data.students, { transaction }); - } - - if (data.discussion_boards !== undefined) { - await courses.setDiscussion_boards(data.discussion_boards, { - transaction, - }); - } - - return courses; - } - - static async deleteByIds(ids, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; - - const courses = await db.courses.findAll({ - where: { - id: { - [Op.in]: ids, - }, - }, - transaction, - }); - - await db.sequelize.transaction(async (transaction) => { - for (const record of courses) { - await record.update({ deletedBy: currentUser.id }, { transaction }); - } - for (const record of courses) { - await record.destroy({ transaction }); - } - }); - - return courses; - } - - static async remove(id, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; - - const courses = await db.courses.findByPk(id, options); - - await courses.update( - { - deletedBy: currentUser.id, - }, - { - transaction, - }, - ); - - await courses.destroy({ - transaction, - }); - - return courses; - } - - static async findBy(where, options) { - const transaction = (options && options.transaction) || undefined; - - const courses = await db.courses.findOne({ where }, { transaction }); - - if (!courses) { - return courses; - } - - const output = courses.get({ plain: true }); - - output.analytics_course = await courses.getAnalytics_course({ - transaction, - }); - - output.discussion_boards_course = await courses.getDiscussion_boards_course( - { - transaction, - }, - ); - - output.enrollments_course = await courses.getEnrollments_course({ - transaction, - }); - - output.grades_course = await courses.getGrades_course({ - transaction, - }); - - output.instructors = await courses.getInstructors({ - transaction, - }); - - output.students = await courses.getStudents({ - transaction, - }); - - output.discussion_boards = await courses.getDiscussion_boards({ - transaction, - }); - - return output; - } - - static async findAll(filter, options) { - const limit = filter.limit || 0; - let offset = 0; - let where = {}; - const currentPage = +filter.page; - - offset = currentPage * limit; - - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - - let include = [ - { - model: db.users, - as: 'instructors', - required: false, - }, - - { - model: db.users, - as: 'students', - required: false, - }, - - { - model: db.discussion_boards, - as: 'discussion_boards', - required: false, - }, - ]; - - if (filter) { - if (filter.id) { - where = { - ...where, - ['id']: Utils.uuid(filter.id), - }; - } - - if (filter.title) { - where = { - ...where, - [Op.and]: Utils.ilike('courses', 'title', filter.title), - }; - } - - if (filter.description) { - where = { - ...where, - [Op.and]: Utils.ilike('courses', 'description', filter.description), - }; - } - - if (filter.active !== undefined) { - where = { - ...where, - active: filter.active === true || filter.active === 'true', - }; - } - - if (filter.instructors) { - const searchTerms = filter.instructors.split('|'); - - include = [ - { - model: db.users, - as: 'instructors_filter', - required: searchTerms.length > 0, - where: - searchTerms.length > 0 - ? { - [Op.or]: [ - { - id: { - [Op.in]: searchTerms.map((term) => Utils.uuid(term)), - }, - }, - { - firstName: { - [Op.or]: searchTerms.map((term) => ({ - [Op.iLike]: `%${term}%`, - })), - }, - }, - ], - } - : undefined, - }, - ...include, - ]; - } - - if (filter.students) { - const searchTerms = filter.students.split('|'); - - include = [ - { - model: db.users, - as: 'students_filter', - required: searchTerms.length > 0, - where: - searchTerms.length > 0 - ? { - [Op.or]: [ - { - id: { - [Op.in]: searchTerms.map((term) => Utils.uuid(term)), - }, - }, - { - firstName: { - [Op.or]: searchTerms.map((term) => ({ - [Op.iLike]: `%${term}%`, - })), - }, - }, - ], - } - : undefined, - }, - ...include, - ]; - } - - if (filter.discussion_boards) { - const searchTerms = filter.discussion_boards.split('|'); - - include = [ - { - model: db.discussion_boards, - as: 'discussion_boards_filter', - required: searchTerms.length > 0, - where: - searchTerms.length > 0 - ? { - [Op.or]: [ - { - id: { - [Op.in]: searchTerms.map((term) => Utils.uuid(term)), - }, - }, - { - topic: { - [Op.or]: searchTerms.map((term) => ({ - [Op.iLike]: `%${term}%`, - })), - }, - }, - ], - } - : undefined, - }, - ...include, - ]; - } - - if (filter.createdAtRange) { - const [start, end] = filter.createdAtRange; - - if (start !== undefined && start !== null && start !== '') { - where = { - ...where, - ['createdAt']: { - ...where.createdAt, - [Op.gte]: start, - }, - }; - } - - if (end !== undefined && end !== null && end !== '') { - where = { - ...where, - ['createdAt']: { - ...where.createdAt, - [Op.lte]: end, - }, - }; - } - } - } - - const queryOptions = { - where, - include, - distinct: true, - order: - filter.field && filter.sort - ? [[filter.field, filter.sort]] - : [['createdAt', 'desc']], - transaction: options?.transaction, - logging: console.log, - }; - - if (!options?.countOnly) { - queryOptions.limit = limit ? Number(limit) : undefined; - queryOptions.offset = offset ? Number(offset) : undefined; - } - - try { - const { rows, count } = await db.courses.findAndCountAll(queryOptions); - - return { - rows: options?.countOnly ? [] : rows, - count: count, - }; - } catch (error) { - console.error('Error executing query:', error); - throw error; - } - } - - static async findAllAutocomplete(query, limit, offset) { - let where = {}; - - if (query) { - where = { - [Op.or]: [ - { ['id']: Utils.uuid(query) }, - Utils.ilike('courses', 'title', query), - ], - }; - } - - const records = await db.courses.findAll({ - attributes: ['id', 'title'], - where, - limit: limit ? Number(limit) : undefined, - offset: offset ? Number(offset) : undefined, - orderBy: [['title', 'ASC']], - }); - - return records.map((record) => ({ - id: record.id, - label: record.title, - })); - } -}; diff --git a/backend/src/db/api/discussion_boards.js b/backend/src/db/api/discussion_boards.js deleted file mode 100644 index c2c03a7..0000000 --- a/backend/src/db/api/discussion_boards.js +++ /dev/null @@ -1,355 +0,0 @@ -const db = require('../models'); -const FileDBApi = require('./file'); -const crypto = require('crypto'); -const Utils = require('../utils'); - -const Sequelize = db.Sequelize; -const Op = Sequelize.Op; - -module.exports = class Discussion_boardsDBApi { - static async create(data, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; - - const discussion_boards = await db.discussion_boards.create( - { - id: data.id || undefined, - - topic: data.topic || null, - importHash: data.importHash || null, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { transaction }, - ); - - await discussion_boards.setCourse(data.course || null, { - transaction, - }); - - await discussion_boards.setPosts(data.posts || [], { - transaction, - }); - - return discussion_boards; - } - - static async bulkImport(data, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; - - // Prepare data - wrapping individual data transformations in a map() method - const discussion_boardsData = data.map((item, index) => ({ - id: item.id || undefined, - - topic: item.topic || null, - importHash: item.importHash || null, - createdById: currentUser.id, - updatedById: currentUser.id, - createdAt: new Date(Date.now() + index * 1000), - })); - - // Bulk create items - const discussion_boards = await db.discussion_boards.bulkCreate( - discussion_boardsData, - { transaction }, - ); - - // For each item created, replace relation files - - return discussion_boards; - } - - static async update(id, data, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; - - const discussion_boards = await db.discussion_boards.findByPk( - id, - {}, - { transaction }, - ); - - const updatePayload = {}; - - if (data.topic !== undefined) updatePayload.topic = data.topic; - - updatePayload.updatedById = currentUser.id; - - await discussion_boards.update(updatePayload, { transaction }); - - if (data.course !== undefined) { - await discussion_boards.setCourse( - data.course, - - { transaction }, - ); - } - - if (data.posts !== undefined) { - await discussion_boards.setPosts(data.posts, { transaction }); - } - - return discussion_boards; - } - - static async deleteByIds(ids, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; - - const discussion_boards = await db.discussion_boards.findAll({ - where: { - id: { - [Op.in]: ids, - }, - }, - transaction, - }); - - await db.sequelize.transaction(async (transaction) => { - for (const record of discussion_boards) { - await record.update({ deletedBy: currentUser.id }, { transaction }); - } - for (const record of discussion_boards) { - await record.destroy({ transaction }); - } - }); - - return discussion_boards; - } - - static async remove(id, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; - - const discussion_boards = await db.discussion_boards.findByPk(id, options); - - await discussion_boards.update( - { - deletedBy: currentUser.id, - }, - { - transaction, - }, - ); - - await discussion_boards.destroy({ - transaction, - }); - - return discussion_boards; - } - - static async findBy(where, options) { - const transaction = (options && options.transaction) || undefined; - - const discussion_boards = await db.discussion_boards.findOne( - { where }, - { transaction }, - ); - - if (!discussion_boards) { - return discussion_boards; - } - - const output = discussion_boards.get({ plain: true }); - - output.posts_discussion_board = - await discussion_boards.getPosts_discussion_board({ - transaction, - }); - - output.course = await discussion_boards.getCourse({ - transaction, - }); - - output.posts = await discussion_boards.getPosts({ - transaction, - }); - - return output; - } - - static async findAll(filter, options) { - const limit = filter.limit || 0; - let offset = 0; - let where = {}; - const currentPage = +filter.page; - - offset = currentPage * limit; - - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - - let include = [ - { - model: db.courses, - as: 'course', - - where: filter.course - ? { - [Op.or]: [ - { - id: { - [Op.in]: filter.course - .split('|') - .map((term) => Utils.uuid(term)), - }, - }, - { - title: { - [Op.or]: filter.course - .split('|') - .map((term) => ({ [Op.iLike]: `%${term}%` })), - }, - }, - ], - } - : {}, - }, - - { - model: db.posts, - as: 'posts', - required: false, - }, - ]; - - if (filter) { - if (filter.id) { - where = { - ...where, - ['id']: Utils.uuid(filter.id), - }; - } - - if (filter.topic) { - where = { - ...where, - [Op.and]: Utils.ilike('discussion_boards', 'topic', filter.topic), - }; - } - - if (filter.active !== undefined) { - where = { - ...where, - active: filter.active === true || filter.active === 'true', - }; - } - - if (filter.posts) { - const searchTerms = filter.posts.split('|'); - - include = [ - { - model: db.posts, - as: 'posts_filter', - required: searchTerms.length > 0, - where: - searchTerms.length > 0 - ? { - [Op.or]: [ - { - id: { - [Op.in]: searchTerms.map((term) => Utils.uuid(term)), - }, - }, - { - content: { - [Op.or]: searchTerms.map((term) => ({ - [Op.iLike]: `%${term}%`, - })), - }, - }, - ], - } - : undefined, - }, - ...include, - ]; - } - - if (filter.createdAtRange) { - const [start, end] = filter.createdAtRange; - - if (start !== undefined && start !== null && start !== '') { - where = { - ...where, - ['createdAt']: { - ...where.createdAt, - [Op.gte]: start, - }, - }; - } - - if (end !== undefined && end !== null && end !== '') { - where = { - ...where, - ['createdAt']: { - ...where.createdAt, - [Op.lte]: end, - }, - }; - } - } - } - - const queryOptions = { - where, - include, - distinct: true, - order: - filter.field && filter.sort - ? [[filter.field, filter.sort]] - : [['createdAt', 'desc']], - transaction: options?.transaction, - logging: console.log, - }; - - if (!options?.countOnly) { - queryOptions.limit = limit ? Number(limit) : undefined; - queryOptions.offset = offset ? Number(offset) : undefined; - } - - try { - const { rows, count } = await db.discussion_boards.findAndCountAll( - queryOptions, - ); - - return { - rows: options?.countOnly ? [] : rows, - count: count, - }; - } catch (error) { - console.error('Error executing query:', error); - throw error; - } - } - - static async findAllAutocomplete(query, limit, offset) { - let where = {}; - - if (query) { - where = { - [Op.or]: [ - { ['id']: Utils.uuid(query) }, - Utils.ilike('discussion_boards', 'topic', query), - ], - }; - } - - const records = await db.discussion_boards.findAll({ - attributes: ['id', 'topic'], - where, - limit: limit ? Number(limit) : undefined, - offset: offset ? Number(offset) : undefined, - orderBy: [['topic', 'ASC']], - }); - - return records.map((record) => ({ - id: record.id, - label: record.topic, - })); - } -}; diff --git a/backend/src/db/api/posts.js b/backend/src/db/api/mcs_pyq.js similarity index 53% rename from backend/src/db/api/posts.js rename to backend/src/db/api/mcs_pyq.js index 0a7c782..84742b1 100644 --- a/backend/src/db/api/posts.js +++ b/backend/src/db/api/mcs_pyq.js @@ -6,17 +6,15 @@ const Utils = require('../utils'); const Sequelize = db.Sequelize; const Op = Sequelize.Op; -module.exports = class PostsDBApi { +module.exports = class Mcs_pyqDBApi { static async create(data, options) { const currentUser = (options && options.currentUser) || { id: null }; const transaction = (options && options.transaction) || undefined; - const posts = await db.posts.create( + const mcs_pyq = await db.mcs_pyq.create( { id: data.id || undefined, - content: data.content || null, - posted_at: data.posted_at || null, importHash: data.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -24,15 +22,7 @@ module.exports = class PostsDBApi { { transaction }, ); - await posts.setDiscussion_board(data.discussion_board || null, { - transaction, - }); - - await posts.setUser(data.user || null, { - transaction, - }); - - return posts; + return mcs_pyq; } static async bulkImport(data, options) { @@ -40,11 +30,9 @@ module.exports = class PostsDBApi { const transaction = (options && options.transaction) || undefined; // Prepare data - wrapping individual data transformations in a map() method - const postsData = data.map((item, index) => ({ + const mcs_pyqData = data.map((item, index) => ({ id: item.id || undefined, - content: item.content || null, - posted_at: item.posted_at || null, importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, @@ -52,53 +40,33 @@ module.exports = class PostsDBApi { })); // Bulk create items - const posts = await db.posts.bulkCreate(postsData, { transaction }); + const mcs_pyq = await db.mcs_pyq.bulkCreate(mcs_pyqData, { transaction }); // For each item created, replace relation files - return posts; + return mcs_pyq; } static async update(id, data, options) { const currentUser = (options && options.currentUser) || { id: null }; const transaction = (options && options.transaction) || undefined; - const posts = await db.posts.findByPk(id, {}, { transaction }); + const mcs_pyq = await db.mcs_pyq.findByPk(id, {}, { transaction }); const updatePayload = {}; - if (data.content !== undefined) updatePayload.content = data.content; - - if (data.posted_at !== undefined) updatePayload.posted_at = data.posted_at; - updatePayload.updatedById = currentUser.id; - await posts.update(updatePayload, { transaction }); + await mcs_pyq.update(updatePayload, { transaction }); - if (data.discussion_board !== undefined) { - await posts.setDiscussion_board( - data.discussion_board, - - { transaction }, - ); - } - - if (data.user !== undefined) { - await posts.setUser( - data.user, - - { transaction }, - ); - } - - return posts; + return mcs_pyq; } static async deleteByIds(ids, options) { const currentUser = (options && options.currentUser) || { id: null }; const transaction = (options && options.transaction) || undefined; - const posts = await db.posts.findAll({ + const mcs_pyq = await db.mcs_pyq.findAll({ where: { id: { [Op.in]: ids, @@ -108,24 +76,24 @@ module.exports = class PostsDBApi { }); await db.sequelize.transaction(async (transaction) => { - for (const record of posts) { + for (const record of mcs_pyq) { await record.update({ deletedBy: currentUser.id }, { transaction }); } - for (const record of posts) { + for (const record of mcs_pyq) { await record.destroy({ transaction }); } }); - return posts; + return mcs_pyq; } static async remove(id, options) { const currentUser = (options && options.currentUser) || { id: null }; const transaction = (options && options.transaction) || undefined; - const posts = await db.posts.findByPk(id, options); + const mcs_pyq = await db.mcs_pyq.findByPk(id, options); - await posts.update( + await mcs_pyq.update( { deletedBy: currentUser.id, }, @@ -134,31 +102,23 @@ module.exports = class PostsDBApi { }, ); - await posts.destroy({ + await mcs_pyq.destroy({ transaction, }); - return posts; + return mcs_pyq; } static async findBy(where, options) { const transaction = (options && options.transaction) || undefined; - const posts = await db.posts.findOne({ where }, { transaction }); + const mcs_pyq = await db.mcs_pyq.findOne({ where }, { transaction }); - if (!posts) { - return posts; + if (!mcs_pyq) { + return mcs_pyq; } - const output = posts.get({ plain: true }); - - output.discussion_board = await posts.getDiscussion_board({ - transaction, - }); - - output.user = await posts.getUser({ - transaction, - }); + const output = mcs_pyq.get({ plain: true }); return output; } @@ -175,59 +135,7 @@ module.exports = class PostsDBApi { const transaction = (options && options.transaction) || undefined; - let include = [ - { - model: db.discussion_boards, - as: 'discussion_board', - - where: filter.discussion_board - ? { - [Op.or]: [ - { - id: { - [Op.in]: filter.discussion_board - .split('|') - .map((term) => Utils.uuid(term)), - }, - }, - { - topic: { - [Op.or]: filter.discussion_board - .split('|') - .map((term) => ({ [Op.iLike]: `%${term}%` })), - }, - }, - ], - } - : {}, - }, - - { - model: db.users, - as: 'user', - - where: filter.user - ? { - [Op.or]: [ - { - id: { - [Op.in]: filter.user - .split('|') - .map((term) => Utils.uuid(term)), - }, - }, - { - firstName: { - [Op.or]: filter.user - .split('|') - .map((term) => ({ [Op.iLike]: `%${term}%` })), - }, - }, - ], - } - : {}, - }, - ]; + let include = []; if (filter) { if (filter.id) { @@ -237,37 +145,6 @@ module.exports = class PostsDBApi { }; } - if (filter.content) { - where = { - ...where, - [Op.and]: Utils.ilike('posts', 'content', filter.content), - }; - } - - if (filter.posted_atRange) { - const [start, end] = filter.posted_atRange; - - if (start !== undefined && start !== null && start !== '') { - where = { - ...where, - posted_at: { - ...where.posted_at, - [Op.gte]: start, - }, - }; - } - - if (end !== undefined && end !== null && end !== '') { - where = { - ...where, - posted_at: { - ...where.posted_at, - [Op.lte]: end, - }, - }; - } - } - if (filter.active !== undefined) { where = { ...where, @@ -318,7 +195,7 @@ module.exports = class PostsDBApi { } try { - const { rows, count } = await db.posts.findAndCountAll(queryOptions); + const { rows, count } = await db.mcs_pyq.findAndCountAll(queryOptions); return { rows: options?.countOnly ? [] : rows, @@ -337,22 +214,22 @@ module.exports = class PostsDBApi { where = { [Op.or]: [ { ['id']: Utils.uuid(query) }, - Utils.ilike('posts', 'content', query), + Utils.ilike('mcs_pyq', 'id', query), ], }; } - const records = await db.posts.findAll({ - attributes: ['id', 'content'], + const records = await db.mcs_pyq.findAll({ + attributes: ['id', 'id'], where, limit: limit ? Number(limit) : undefined, offset: offset ? Number(offset) : undefined, - orderBy: [['content', 'ASC']], + orderBy: [['id', 'ASC']], }); return records.map((record) => ({ id: record.id, - label: record.content, + label: record.id, })); } }; diff --git a/backend/src/db/api/students.js b/backend/src/db/api/students.js deleted file mode 100644 index 5b4c8f4..0000000 --- a/backend/src/db/api/students.js +++ /dev/null @@ -1,387 +0,0 @@ -const db = require('../models'); -const FileDBApi = require('./file'); -const crypto = require('crypto'); -const Utils = require('../utils'); - -const Sequelize = db.Sequelize; -const Op = Sequelize.Op; - -module.exports = class StudentsDBApi { - static async create(data, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; - - const students = await db.students.create( - { - id: data.id || undefined, - - importHash: data.importHash || null, - createdById: currentUser.id, - updatedById: currentUser.id, - }, - { transaction }, - ); - - await students.setUser(data.user || null, { - transaction, - }); - - await students.setEnrollments(data.enrollments || [], { - transaction, - }); - - await students.setGrades(data.grades || [], { - transaction, - }); - - return students; - } - - static async bulkImport(data, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; - - // Prepare data - wrapping individual data transformations in a map() method - const studentsData = data.map((item, index) => ({ - id: item.id || undefined, - - importHash: item.importHash || null, - createdById: currentUser.id, - updatedById: currentUser.id, - createdAt: new Date(Date.now() + index * 1000), - })); - - // Bulk create items - const students = await db.students.bulkCreate(studentsData, { - transaction, - }); - - // For each item created, replace relation files - - return students; - } - - static async update(id, data, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; - - const students = await db.students.findByPk(id, {}, { transaction }); - - const updatePayload = {}; - - updatePayload.updatedById = currentUser.id; - - await students.update(updatePayload, { transaction }); - - if (data.user !== undefined) { - await students.setUser( - data.user, - - { transaction }, - ); - } - - if (data.enrollments !== undefined) { - await students.setEnrollments(data.enrollments, { transaction }); - } - - if (data.grades !== undefined) { - await students.setGrades(data.grades, { transaction }); - } - - return students; - } - - static async deleteByIds(ids, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; - - const students = await db.students.findAll({ - where: { - id: { - [Op.in]: ids, - }, - }, - transaction, - }); - - await db.sequelize.transaction(async (transaction) => { - for (const record of students) { - await record.update({ deletedBy: currentUser.id }, { transaction }); - } - for (const record of students) { - await record.destroy({ transaction }); - } - }); - - return students; - } - - static async remove(id, options) { - const currentUser = (options && options.currentUser) || { id: null }; - const transaction = (options && options.transaction) || undefined; - - const students = await db.students.findByPk(id, options); - - await students.update( - { - deletedBy: currentUser.id, - }, - { - transaction, - }, - ); - - await students.destroy({ - transaction, - }); - - return students; - } - - static async findBy(where, options) { - const transaction = (options && options.transaction) || undefined; - - const students = await db.students.findOne({ where }, { transaction }); - - if (!students) { - return students; - } - - const output = students.get({ plain: true }); - - output.enrollments_student = await students.getEnrollments_student({ - transaction, - }); - - output.grades_student = await students.getGrades_student({ - transaction, - }); - - output.user = await students.getUser({ - transaction, - }); - - output.enrollments = await students.getEnrollments({ - transaction, - }); - - output.grades = await students.getGrades({ - transaction, - }); - - return output; - } - - static async findAll(filter, options) { - const limit = filter.limit || 0; - let offset = 0; - let where = {}; - const currentPage = +filter.page; - - offset = currentPage * limit; - - const orderBy = null; - - const transaction = (options && options.transaction) || undefined; - - let include = [ - { - model: db.users, - as: 'user', - - where: filter.user - ? { - [Op.or]: [ - { - id: { - [Op.in]: filter.user - .split('|') - .map((term) => Utils.uuid(term)), - }, - }, - { - firstName: { - [Op.or]: filter.user - .split('|') - .map((term) => ({ [Op.iLike]: `%${term}%` })), - }, - }, - ], - } - : {}, - }, - - { - model: db.enrollments, - as: 'enrollments', - required: false, - }, - - { - model: db.grades, - as: 'grades', - required: false, - }, - ]; - - if (filter) { - if (filter.id) { - where = { - ...where, - ['id']: Utils.uuid(filter.id), - }; - } - - if (filter.active !== undefined) { - where = { - ...where, - active: filter.active === true || filter.active === 'true', - }; - } - - if (filter.enrollments) { - const searchTerms = filter.enrollments.split('|'); - - include = [ - { - model: db.enrollments, - as: 'enrollments_filter', - required: searchTerms.length > 0, - where: - searchTerms.length > 0 - ? { - [Op.or]: [ - { - id: { - [Op.in]: searchTerms.map((term) => Utils.uuid(term)), - }, - }, - { - course: { - [Op.or]: searchTerms.map((term) => ({ - [Op.iLike]: `%${term}%`, - })), - }, - }, - ], - } - : undefined, - }, - ...include, - ]; - } - - if (filter.grades) { - const searchTerms = filter.grades.split('|'); - - include = [ - { - model: db.grades, - as: 'grades_filter', - required: searchTerms.length > 0, - where: - searchTerms.length > 0 - ? { - [Op.or]: [ - { - id: { - [Op.in]: searchTerms.map((term) => Utils.uuid(term)), - }, - }, - { - grade: { - [Op.or]: searchTerms.map((term) => ({ - [Op.iLike]: `%${term}%`, - })), - }, - }, - ], - } - : undefined, - }, - ...include, - ]; - } - - if (filter.createdAtRange) { - const [start, end] = filter.createdAtRange; - - if (start !== undefined && start !== null && start !== '') { - where = { - ...where, - ['createdAt']: { - ...where.createdAt, - [Op.gte]: start, - }, - }; - } - - if (end !== undefined && end !== null && end !== '') { - where = { - ...where, - ['createdAt']: { - ...where.createdAt, - [Op.lte]: end, - }, - }; - } - } - } - - const queryOptions = { - where, - include, - distinct: true, - order: - filter.field && filter.sort - ? [[filter.field, filter.sort]] - : [['createdAt', 'desc']], - transaction: options?.transaction, - logging: console.log, - }; - - if (!options?.countOnly) { - queryOptions.limit = limit ? Number(limit) : undefined; - queryOptions.offset = offset ? Number(offset) : undefined; - } - - try { - const { rows, count } = await db.students.findAndCountAll(queryOptions); - - return { - rows: options?.countOnly ? [] : rows, - count: count, - }; - } catch (error) { - console.error('Error executing query:', error); - throw error; - } - } - - static async findAllAutocomplete(query, limit, offset) { - let where = {}; - - if (query) { - where = { - [Op.or]: [ - { ['id']: Utils.uuid(query) }, - Utils.ilike('students', 'user', query), - ], - }; - } - - const records = await db.students.findAll({ - attributes: ['id', 'user'], - where, - limit: limit ? Number(limit) : undefined, - offset: offset ? Number(offset) : undefined, - orderBy: [['user', 'ASC']], - }); - - return records.map((record) => ({ - id: record.id, - label: record.user, - })); - } -}; diff --git a/backend/src/db/api/users.js b/backend/src/db/api/users.js index 71d3dee..3a11e69 100644 --- a/backend/src/db/api/users.js +++ b/backend/src/db/api/users.js @@ -267,18 +267,6 @@ module.exports = class UsersDBApi { const output = users.get({ plain: true }); - output.instructors_user = await users.getInstructors_user({ - transaction, - }); - - output.posts_user = await users.getPosts_user({ - transaction, - }); - - output.students_user = await users.getStudents_user({ - transaction, - }); - output.avatar = await users.getAvatar({ transaction, }); diff --git a/backend/src/db/migrations/1755629794386.js b/backend/src/db/migrations/1755629794386.js new file mode 100644 index 0000000..ff1855b --- /dev/null +++ b/backend/src/db/migrations/1755629794386.js @@ -0,0 +1,396 @@ +module.exports = { + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async up(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.createTable( + 'mcs_pyq', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'course', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.dropTable('courses', { transaction }); + + await queryInterface.dropTable('analytics', { transaction }); + + await queryInterface.dropTable('discussion_boards', { transaction }); + + await queryInterface.dropTable('enrollments', { transaction }); + + await queryInterface.dropTable('grades', { transaction }); + + await queryInterface.dropTable('instructors', { transaction }); + + await queryInterface.dropTable('posts', { transaction }); + + await queryInterface.dropTable('students', { transaction }); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, + /** + * @param {QueryInterface} queryInterface + * @param {Sequelize} Sequelize + * @returns {Promise} + */ + async down(queryInterface, Sequelize) { + /** + * @type {Transaction} + */ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.dropTable('course', { transaction }); + + await queryInterface.dropTable('mcs_pyq', { transaction }); + + await queryInterface.createTable( + 'students', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'posts', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'instructors', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'grades', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'enrollments', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'discussion_boards', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'analytics', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await queryInterface.createTable( + 'courses', + { + id: { + type: Sequelize.DataTypes.UUID, + defaultValue: Sequelize.DataTypes.UUIDV4, + primaryKey: true, + }, + createdById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + updatedById: { + type: Sequelize.DataTypes.UUID, + references: { + key: 'id', + model: 'users', + }, + }, + createdAt: { type: Sequelize.DataTypes.DATE }, + updatedAt: { type: Sequelize.DataTypes.DATE }, + deletedAt: { type: Sequelize.DataTypes.DATE }, + importHash: { + type: Sequelize.DataTypes.STRING(255), + allowNull: true, + unique: true, + }, + }, + { transaction }, + ); + + await transaction.commit(); + } catch (err) { + await transaction.rollback(); + throw err; + } + }, +}; diff --git a/backend/src/db/models/posts.js b/backend/src/db/models/course.js similarity index 56% rename from backend/src/db/models/posts.js rename to backend/src/db/models/course.js index 9040f6b..09da4d9 100644 --- a/backend/src/db/models/posts.js +++ b/backend/src/db/models/course.js @@ -5,8 +5,8 @@ const bcrypt = require('bcrypt'); const moment = require('moment'); module.exports = function (sequelize, DataTypes) { - const posts = sequelize.define( - 'posts', + const course = sequelize.define( + 'course', { id: { type: DataTypes.UUID, @@ -14,14 +14,6 @@ module.exports = function (sequelize, DataTypes) { primaryKey: true, }, - content: { - type: DataTypes.TEXT, - }, - - posted_at: { - type: DataTypes.DATE, - }, - importHash: { type: DataTypes.STRING(255), allowNull: true, @@ -35,35 +27,19 @@ module.exports = function (sequelize, DataTypes) { }, ); - posts.associate = (db) => { + course.associate = (db) => { /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity //end loop - db.posts.belongsTo(db.discussion_boards, { - as: 'discussion_board', - foreignKey: { - name: 'discussion_boardId', - }, - constraints: false, - }); - - db.posts.belongsTo(db.users, { - as: 'user', - foreignKey: { - name: 'userId', - }, - constraints: false, - }); - - db.posts.belongsTo(db.users, { + db.course.belongsTo(db.users, { as: 'createdBy', }); - db.posts.belongsTo(db.users, { + db.course.belongsTo(db.users, { as: 'updatedBy', }); }; - return posts; + return course; }; diff --git a/backend/src/db/models/courses.js b/backend/src/db/models/courses.js deleted file mode 100644 index bb99744..0000000 --- a/backend/src/db/models/courses.js +++ /dev/null @@ -1,139 +0,0 @@ -const config = require('../../config'); -const providers = config.providers; -const crypto = require('crypto'); -const bcrypt = require('bcrypt'); -const moment = require('moment'); - -module.exports = function (sequelize, DataTypes) { - const courses = sequelize.define( - 'courses', - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - - title: { - type: DataTypes.TEXT, - }, - - description: { - type: DataTypes.TEXT, - }, - - importHash: { - type: DataTypes.STRING(255), - allowNull: true, - unique: true, - }, - }, - { - timestamps: true, - paranoid: true, - freezeTableName: true, - }, - ); - - courses.associate = (db) => { - db.courses.belongsToMany(db.users, { - as: 'instructors', - foreignKey: { - name: 'courses_instructorsId', - }, - constraints: false, - through: 'coursesInstructorsUsers', - }); - - db.courses.belongsToMany(db.users, { - as: 'instructors_filter', - foreignKey: { - name: 'courses_instructorsId', - }, - constraints: false, - through: 'coursesInstructorsUsers', - }); - - db.courses.belongsToMany(db.users, { - as: 'students', - foreignKey: { - name: 'courses_studentsId', - }, - constraints: false, - through: 'coursesStudentsUsers', - }); - - db.courses.belongsToMany(db.users, { - as: 'students_filter', - foreignKey: { - name: 'courses_studentsId', - }, - constraints: false, - through: 'coursesStudentsUsers', - }); - - db.courses.belongsToMany(db.discussion_boards, { - as: 'discussion_boards', - foreignKey: { - name: 'courses_discussion_boardsId', - }, - constraints: false, - through: 'coursesDiscussion_boardsDiscussion_boards', - }); - - db.courses.belongsToMany(db.discussion_boards, { - as: 'discussion_boards_filter', - foreignKey: { - name: 'courses_discussion_boardsId', - }, - constraints: false, - through: 'coursesDiscussion_boardsDiscussion_boards', - }); - - /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - db.courses.hasMany(db.analytics, { - as: 'analytics_course', - foreignKey: { - name: 'courseId', - }, - constraints: false, - }); - - db.courses.hasMany(db.discussion_boards, { - as: 'discussion_boards_course', - foreignKey: { - name: 'courseId', - }, - constraints: false, - }); - - db.courses.hasMany(db.enrollments, { - as: 'enrollments_course', - foreignKey: { - name: 'courseId', - }, - constraints: false, - }); - - db.courses.hasMany(db.grades, { - as: 'grades_course', - foreignKey: { - name: 'courseId', - }, - constraints: false, - }); - - //end loop - - db.courses.belongsTo(db.users, { - as: 'createdBy', - }); - - db.courses.belongsTo(db.users, { - as: 'updatedBy', - }); - }; - - return courses; -}; diff --git a/backend/src/db/models/discussion_boards.js b/backend/src/db/models/discussion_boards.js deleted file mode 100644 index dbb3d09..0000000 --- a/backend/src/db/models/discussion_boards.js +++ /dev/null @@ -1,83 +0,0 @@ -const config = require('../../config'); -const providers = config.providers; -const crypto = require('crypto'); -const bcrypt = require('bcrypt'); -const moment = require('moment'); - -module.exports = function (sequelize, DataTypes) { - const discussion_boards = sequelize.define( - 'discussion_boards', - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - - topic: { - type: DataTypes.TEXT, - }, - - importHash: { - type: DataTypes.STRING(255), - allowNull: true, - unique: true, - }, - }, - { - timestamps: true, - paranoid: true, - freezeTableName: true, - }, - ); - - discussion_boards.associate = (db) => { - db.discussion_boards.belongsToMany(db.posts, { - as: 'posts', - foreignKey: { - name: 'discussion_boards_postsId', - }, - constraints: false, - through: 'discussion_boardsPostsPosts', - }); - - db.discussion_boards.belongsToMany(db.posts, { - as: 'posts_filter', - foreignKey: { - name: 'discussion_boards_postsId', - }, - constraints: false, - through: 'discussion_boardsPostsPosts', - }); - - /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - db.discussion_boards.hasMany(db.posts, { - as: 'posts_discussion_board', - foreignKey: { - name: 'discussion_boardId', - }, - constraints: false, - }); - - //end loop - - db.discussion_boards.belongsTo(db.courses, { - as: 'course', - foreignKey: { - name: 'courseId', - }, - constraints: false, - }); - - db.discussion_boards.belongsTo(db.users, { - as: 'createdBy', - }); - - db.discussion_boards.belongsTo(db.users, { - as: 'updatedBy', - }); - }; - - return discussion_boards; -}; diff --git a/backend/src/db/models/instructors.js b/backend/src/db/models/instructors.js deleted file mode 100644 index 699f15f..0000000 --- a/backend/src/db/models/instructors.js +++ /dev/null @@ -1,75 +0,0 @@ -const config = require('../../config'); -const providers = config.providers; -const crypto = require('crypto'); -const bcrypt = require('bcrypt'); -const moment = require('moment'); - -module.exports = function (sequelize, DataTypes) { - const instructors = sequelize.define( - 'instructors', - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - - qualifications: { - type: DataTypes.TEXT, - }, - - importHash: { - type: DataTypes.STRING(255), - allowNull: true, - unique: true, - }, - }, - { - timestamps: true, - paranoid: true, - freezeTableName: true, - }, - ); - - instructors.associate = (db) => { - db.instructors.belongsToMany(db.courses, { - as: 'courses', - foreignKey: { - name: 'instructors_coursesId', - }, - constraints: false, - through: 'instructorsCoursesCourses', - }); - - db.instructors.belongsToMany(db.courses, { - as: 'courses_filter', - foreignKey: { - name: 'instructors_coursesId', - }, - constraints: false, - through: 'instructorsCoursesCourses', - }); - - /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - //end loop - - db.instructors.belongsTo(db.users, { - as: 'user', - foreignKey: { - name: 'userId', - }, - constraints: false, - }); - - db.instructors.belongsTo(db.users, { - as: 'createdBy', - }); - - db.instructors.belongsTo(db.users, { - as: 'updatedBy', - }); - }; - - return instructors; -}; diff --git a/backend/src/db/models/enrollments.js b/backend/src/db/models/mcs_pyq.js similarity index 55% rename from backend/src/db/models/enrollments.js rename to backend/src/db/models/mcs_pyq.js index b1779b6..5f4e161 100644 --- a/backend/src/db/models/enrollments.js +++ b/backend/src/db/models/mcs_pyq.js @@ -5,8 +5,8 @@ const bcrypt = require('bcrypt'); const moment = require('moment'); module.exports = function (sequelize, DataTypes) { - const enrollments = sequelize.define( - 'enrollments', + const mcs_pyq = sequelize.define( + 'mcs_pyq', { id: { type: DataTypes.UUID, @@ -14,12 +14,6 @@ module.exports = function (sequelize, DataTypes) { primaryKey: true, }, - payment_status: { - type: DataTypes.ENUM, - - values: ['pending', 'completed'], - }, - importHash: { type: DataTypes.STRING(255), allowNull: true, @@ -33,35 +27,19 @@ module.exports = function (sequelize, DataTypes) { }, ); - enrollments.associate = (db) => { + mcs_pyq.associate = (db) => { /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity //end loop - db.enrollments.belongsTo(db.students, { - as: 'student', - foreignKey: { - name: 'studentId', - }, - constraints: false, - }); - - db.enrollments.belongsTo(db.courses, { - as: 'course', - foreignKey: { - name: 'courseId', - }, - constraints: false, - }); - - db.enrollments.belongsTo(db.users, { + db.mcs_pyq.belongsTo(db.users, { as: 'createdBy', }); - db.enrollments.belongsTo(db.users, { + db.mcs_pyq.belongsTo(db.users, { as: 'updatedBy', }); }; - return enrollments; + return mcs_pyq; }; diff --git a/backend/src/db/models/students.js b/backend/src/db/models/students.js deleted file mode 100644 index 73787bd..0000000 --- a/backend/src/db/models/students.js +++ /dev/null @@ -1,105 +0,0 @@ -const config = require('../../config'); -const providers = config.providers; -const crypto = require('crypto'); -const bcrypt = require('bcrypt'); -const moment = require('moment'); - -module.exports = function (sequelize, DataTypes) { - const students = sequelize.define( - 'students', - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - - importHash: { - type: DataTypes.STRING(255), - allowNull: true, - unique: true, - }, - }, - { - timestamps: true, - paranoid: true, - freezeTableName: true, - }, - ); - - students.associate = (db) => { - db.students.belongsToMany(db.enrollments, { - as: 'enrollments', - foreignKey: { - name: 'students_enrollmentsId', - }, - constraints: false, - through: 'studentsEnrollmentsEnrollments', - }); - - db.students.belongsToMany(db.enrollments, { - as: 'enrollments_filter', - foreignKey: { - name: 'students_enrollmentsId', - }, - constraints: false, - through: 'studentsEnrollmentsEnrollments', - }); - - db.students.belongsToMany(db.grades, { - as: 'grades', - foreignKey: { - name: 'students_gradesId', - }, - constraints: false, - through: 'studentsGradesGrades', - }); - - db.students.belongsToMany(db.grades, { - as: 'grades_filter', - foreignKey: { - name: 'students_gradesId', - }, - constraints: false, - through: 'studentsGradesGrades', - }); - - /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - - db.students.hasMany(db.enrollments, { - as: 'enrollments_student', - foreignKey: { - name: 'studentId', - }, - constraints: false, - }); - - db.students.hasMany(db.grades, { - as: 'grades_student', - foreignKey: { - name: 'studentId', - }, - constraints: false, - }); - - //end loop - - db.students.belongsTo(db.users, { - as: 'user', - foreignKey: { - name: 'userId', - }, - constraints: false, - }); - - db.students.belongsTo(db.users, { - as: 'createdBy', - }); - - db.students.belongsTo(db.users, { - as: 'updatedBy', - }); - }; - - return students; -}; diff --git a/backend/src/db/models/users.js b/backend/src/db/models/users.js index b1aef69..74e549e 100644 --- a/backend/src/db/models/users.js +++ b/backend/src/db/models/users.js @@ -102,30 +102,6 @@ module.exports = function (sequelize, DataTypes) { /// loop through entities and it's fields, and if ref === current e[name] and create relation has many on parent entity - db.users.hasMany(db.instructors, { - as: 'instructors_user', - foreignKey: { - name: 'userId', - }, - constraints: false, - }); - - db.users.hasMany(db.posts, { - as: 'posts_user', - foreignKey: { - name: 'userId', - }, - constraints: false, - }); - - db.users.hasMany(db.students, { - as: 'students_user', - foreignKey: { - name: 'userId', - }, - constraints: false, - }); - //end loop db.users.belongsTo(db.roles, { diff --git a/backend/src/db/seeders/20200430130760-user-roles.js b/backend/src/db/seeders/20200430130760-user-roles.js index 9a3b18c..36e25f4 100644 --- a/backend/src/db/seeders/20200430130760-user-roles.js +++ b/backend/src/db/seeders/20200430130760-user-roles.js @@ -98,20 +98,7 @@ module.exports = { ]; } - const entities = [ - 'users', - 'analytics', - 'courses', - 'discussion_boards', - 'enrollments', - 'grades', - 'instructors', - 'posts', - 'students', - 'roles', - 'permissions', - , - ]; + const entities = ['users', 'roles', 'permissions', 'mcs_pyq', 'course', ,]; await queryInterface.bulkInsert( 'permissions', entities.flatMap(createPermissions), @@ -221,678 +208,6 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('READ_USERS'), }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('CREATE_ANALYTICS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('READ_ANALYTICS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('UPDATE_ANALYTICS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('DELETE_ANALYTICS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('CourseManager'), - permissionId: getId('CREATE_ANALYTICS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('CourseManager'), - permissionId: getId('READ_ANALYTICS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('CourseManager'), - permissionId: getId('UPDATE_ANALYTICS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('ContentSpecialist'), - permissionId: getId('CREATE_ANALYTICS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('ContentSpecialist'), - permissionId: getId('READ_ANALYTICS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('TeachingAssistant'), - permissionId: getId('CREATE_ANALYTICS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('TeachingAssistant'), - permissionId: getId('READ_ANALYTICS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('Learner'), - permissionId: getId('READ_ANALYTICS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('CREATE_COURSES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('READ_COURSES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('UPDATE_COURSES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('DELETE_COURSES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('CourseManager'), - permissionId: getId('CREATE_COURSES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('CourseManager'), - permissionId: getId('READ_COURSES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('CourseManager'), - permissionId: getId('UPDATE_COURSES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('ContentSpecialist'), - permissionId: getId('CREATE_COURSES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('ContentSpecialist'), - permissionId: getId('READ_COURSES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('TeachingAssistant'), - permissionId: getId('CREATE_COURSES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('TeachingAssistant'), - permissionId: getId('READ_COURSES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('Learner'), - permissionId: getId('READ_COURSES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('CREATE_DISCUSSION_BOARDS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('READ_DISCUSSION_BOARDS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('UPDATE_DISCUSSION_BOARDS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('DELETE_DISCUSSION_BOARDS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('CourseManager'), - permissionId: getId('CREATE_DISCUSSION_BOARDS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('CourseManager'), - permissionId: getId('READ_DISCUSSION_BOARDS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('CourseManager'), - permissionId: getId('UPDATE_DISCUSSION_BOARDS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('ContentSpecialist'), - permissionId: getId('CREATE_DISCUSSION_BOARDS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('ContentSpecialist'), - permissionId: getId('READ_DISCUSSION_BOARDS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('TeachingAssistant'), - permissionId: getId('CREATE_DISCUSSION_BOARDS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('TeachingAssistant'), - permissionId: getId('READ_DISCUSSION_BOARDS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('Learner'), - permissionId: getId('READ_DISCUSSION_BOARDS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('CREATE_ENROLLMENTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('READ_ENROLLMENTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('UPDATE_ENROLLMENTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('DELETE_ENROLLMENTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('CourseManager'), - permissionId: getId('CREATE_ENROLLMENTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('CourseManager'), - permissionId: getId('READ_ENROLLMENTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('CourseManager'), - permissionId: getId('UPDATE_ENROLLMENTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('ContentSpecialist'), - permissionId: getId('CREATE_ENROLLMENTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('ContentSpecialist'), - permissionId: getId('READ_ENROLLMENTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('TeachingAssistant'), - permissionId: getId('CREATE_ENROLLMENTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('TeachingAssistant'), - permissionId: getId('READ_ENROLLMENTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('Learner'), - permissionId: getId('READ_ENROLLMENTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('CREATE_GRADES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('READ_GRADES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('UPDATE_GRADES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('DELETE_GRADES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('CourseManager'), - permissionId: getId('CREATE_GRADES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('CourseManager'), - permissionId: getId('READ_GRADES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('CourseManager'), - permissionId: getId('UPDATE_GRADES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('ContentSpecialist'), - permissionId: getId('CREATE_GRADES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('ContentSpecialist'), - permissionId: getId('READ_GRADES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('TeachingAssistant'), - permissionId: getId('CREATE_GRADES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('TeachingAssistant'), - permissionId: getId('READ_GRADES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('Learner'), - permissionId: getId('READ_GRADES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('CREATE_INSTRUCTORS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('READ_INSTRUCTORS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('UPDATE_INSTRUCTORS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('DELETE_INSTRUCTORS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('CourseManager'), - permissionId: getId('CREATE_INSTRUCTORS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('CourseManager'), - permissionId: getId('READ_INSTRUCTORS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('CourseManager'), - permissionId: getId('UPDATE_INSTRUCTORS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('ContentSpecialist'), - permissionId: getId('CREATE_INSTRUCTORS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('ContentSpecialist'), - permissionId: getId('READ_INSTRUCTORS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('TeachingAssistant'), - permissionId: getId('CREATE_INSTRUCTORS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('TeachingAssistant'), - permissionId: getId('READ_INSTRUCTORS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('Learner'), - permissionId: getId('READ_INSTRUCTORS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('CREATE_POSTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('READ_POSTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('UPDATE_POSTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('DELETE_POSTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('CourseManager'), - permissionId: getId('CREATE_POSTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('CourseManager'), - permissionId: getId('READ_POSTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('CourseManager'), - permissionId: getId('UPDATE_POSTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('ContentSpecialist'), - permissionId: getId('CREATE_POSTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('ContentSpecialist'), - permissionId: getId('READ_POSTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('TeachingAssistant'), - permissionId: getId('CREATE_POSTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('TeachingAssistant'), - permissionId: getId('READ_POSTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('Learner'), - permissionId: getId('READ_POSTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('CREATE_STUDENTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('READ_STUDENTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('UPDATE_STUDENTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('EducationDirector'), - permissionId: getId('DELETE_STUDENTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('CourseManager'), - permissionId: getId('CREATE_STUDENTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('CourseManager'), - permissionId: getId('READ_STUDENTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('CourseManager'), - permissionId: getId('UPDATE_STUDENTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('ContentSpecialist'), - permissionId: getId('CREATE_STUDENTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('ContentSpecialist'), - permissionId: getId('READ_STUDENTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('TeachingAssistant'), - permissionId: getId('CREATE_STUDENTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('TeachingAssistant'), - permissionId: getId('READ_STUDENTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('Learner'), - permissionId: getId('READ_STUDENTS'), - }, - { createdAt, updatedAt, @@ -953,206 +268,6 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_USERS'), }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('CREATE_ANALYTICS'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('READ_ANALYTICS'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('UPDATE_ANALYTICS'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('DELETE_ANALYTICS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('CREATE_COURSES'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('READ_COURSES'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('UPDATE_COURSES'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('DELETE_COURSES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('CREATE_DISCUSSION_BOARDS'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('READ_DISCUSSION_BOARDS'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('UPDATE_DISCUSSION_BOARDS'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('DELETE_DISCUSSION_BOARDS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('CREATE_ENROLLMENTS'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('READ_ENROLLMENTS'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('UPDATE_ENROLLMENTS'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('DELETE_ENROLLMENTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('CREATE_GRADES'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('READ_GRADES'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('UPDATE_GRADES'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('DELETE_GRADES'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('CREATE_INSTRUCTORS'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('READ_INSTRUCTORS'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('UPDATE_INSTRUCTORS'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('DELETE_INSTRUCTORS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('CREATE_POSTS'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('READ_POSTS'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('UPDATE_POSTS'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('DELETE_POSTS'), - }, - - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('CREATE_STUDENTS'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('READ_STUDENTS'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('UPDATE_STUDENTS'), - }, - { - createdAt, - updatedAt, - roles_permissionsId: getId('Administrator'), - permissionId: getId('DELETE_STUDENTS'), - }, - { createdAt, updatedAt, @@ -1203,6 +318,56 @@ primary key ("roles_permissionsId", "permissionId") permissionId: getId('DELETE_PERMISSIONS'), }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_MCS_PYQ'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_MCS_PYQ'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_MCS_PYQ'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_MCS_PYQ'), + }, + + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('CREATE_COURSE'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('READ_COURSE'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('UPDATE_COURSE'), + }, + { + createdAt, + updatedAt, + roles_permissionsId: getId('Administrator'), + permissionId: getId('DELETE_COURSE'), + }, + { createdAt, updatedAt, diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index c4e12a8..396ce50 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -1,1046 +1,30 @@ const db = require('../models'); const Users = db.users; -const Analytics = db.analytics; +const McsPyq = db.mcs_pyq; -const Courses = db.courses; +const Course = db.course; -const DiscussionBoards = db.discussion_boards; +const McsPyqData = [{}, {}, {}, {}]; -const Enrollments = db.enrollments; - -const Grades = db.grades; - -const Instructors = db.instructors; - -const Posts = db.posts; - -const Students = db.students; - -const AnalyticsData = [ - { - // type code here for "relation_one" field - - student_engagement: 75, - - completion_rate: 80, - - instructor_performance: 85, - }, - - { - // type code here for "relation_one" field - - student_engagement: 70, - - completion_rate: 75, - - instructor_performance: 80, - }, - - { - // type code here for "relation_one" field - - student_engagement: 85, - - completion_rate: 90, - - instructor_performance: 95, - }, - - { - // type code here for "relation_one" field - - student_engagement: 80, - - completion_rate: 85, - - instructor_performance: 90, - }, - - { - // type code here for "relation_one" field - - student_engagement: 78, - - completion_rate: 82, - - instructor_performance: 88, - }, -]; - -const CoursesData = [ - { - title: 'Introduction to Programming', - - description: 'Learn the basics of programming with Python.', - - // type code here for "relation_many" field - - // type code here for "relation_many" field - - // type code here for "relation_many" field - }, - - { - title: 'Advanced Data Structures', - - description: 'Explore complex data structures and algorithms.', - - // type code here for "relation_many" field - - // type code here for "relation_many" field - - // type code here for "relation_many" field - }, - - { - title: 'Web Development Bootcamp', - - description: 'Full-stack web development with React and Node.js.', - - // type code here for "relation_many" field - - // type code here for "relation_many" field - - // type code here for "relation_many" field - }, - - { - title: 'Machine Learning Essentials', - - description: 'Understand the fundamentals of machine learning.', - - // type code here for "relation_many" field - - // type code here for "relation_many" field - - // type code here for "relation_many" field - }, - - { - title: 'Database Management Systems', - - description: 'Comprehensive guide to managing databases with SQL.', - - // type code here for "relation_many" field - - // type code here for "relation_many" field - - // type code here for "relation_many" field - }, -]; - -const DiscussionBoardsData = [ - { - // type code here for "relation_one" field - - topic: 'Python Basics', - - // type code here for "relation_many" field - }, - - { - // type code here for "relation_one" field - - topic: 'Algorithm Optimization', - - // type code here for "relation_many" field - }, - - { - // type code here for "relation_one" field - - topic: 'React Components', - - // type code here for "relation_many" field - }, - - { - // type code here for "relation_one" field - - topic: 'Supervised Learning', - - // type code here for "relation_many" field - }, - - { - // type code here for "relation_one" field - - topic: 'SQL Queries', - - // type code here for "relation_many" field - }, -]; - -const EnrollmentsData = [ - { - // type code here for "relation_one" field - - // type code here for "relation_one" field - - payment_status: 'completed', - }, - - { - // type code here for "relation_one" field - - // type code here for "relation_one" field - - payment_status: 'completed', - }, - - { - // type code here for "relation_one" field - - // type code here for "relation_one" field - - payment_status: 'pending', - }, - - { - // type code here for "relation_one" field - - // type code here for "relation_one" field - - payment_status: 'pending', - }, - - { - // type code here for "relation_one" field - - // type code here for "relation_one" field - - payment_status: 'pending', - }, -]; - -const GradesData = [ - { - // type code here for "relation_one" field - - // type code here for "relation_one" field - - grade: 85.5, - }, - - { - // type code here for "relation_one" field - - // type code here for "relation_one" field - - grade: 90, - }, - - { - // type code here for "relation_one" field - - // type code here for "relation_one" field - - grade: 88, - }, - - { - // type code here for "relation_one" field - - // type code here for "relation_one" field - - grade: 92, - }, - - { - // type code here for "relation_one" field - - // type code here for "relation_one" field - - grade: 87.5, - }, -]; - -const InstructorsData = [ - { - // type code here for "relation_one" field - - qualifications: 'PhD in Computer Science', - - // type code here for "relation_many" field - }, - - { - // type code here for "relation_one" field - - qualifications: 'MSc in Data Science', - - // type code here for "relation_many" field - }, - - { - // type code here for "relation_one" field - - qualifications: 'BSc in Information Technology', - - // type code here for "relation_many" field - }, - - { - // type code here for "relation_one" field - - qualifications: 'MSc in Software Engineering', - - // type code here for "relation_many" field - }, - - { - // type code here for "relation_one" field - - qualifications: 'BSc in Mathematics', - - // type code here for "relation_many" field - }, -]; - -const PostsData = [ - { - // type code here for "relation_one" field - - // type code here for "relation_one" field - - content: 'What is a variable in Python?', - - posted_at: new Date('2023-10-01T10:00:00Z'), - }, - - { - // type code here for "relation_one" field - - // type code here for "relation_one" field - - content: 'How can we improve sorting algorithms?', - - posted_at: new Date('2023-10-02T11:00:00Z'), - }, - - { - // type code here for "relation_one" field - - // type code here for "relation_one" field - - content: 'How to manage state in React?', - - posted_at: new Date('2023-10-03T12:00:00Z'), - }, - - { - // type code here for "relation_one" field - - // type code here for "relation_one" field - - content: 'What is the difference between regression and classification?', - - posted_at: new Date('2023-10-04T13:00:00Z'), - }, - - { - // type code here for "relation_one" field - - // type code here for "relation_one" field - - content: 'How to join tables in SQL?', - - posted_at: new Date('2023-10-05T14:00:00Z'), - }, -]; - -const StudentsData = [ - { - // type code here for "relation_one" field - // type code here for "relation_many" field - // type code here for "relation_many" field - }, - - { - // type code here for "relation_one" field - // type code here for "relation_many" field - // type code here for "relation_many" field - }, - - { - // type code here for "relation_one" field - // type code here for "relation_many" field - // type code here for "relation_many" field - }, - - { - // type code here for "relation_one" field - // type code here for "relation_many" field - // type code here for "relation_many" field - }, - - { - // type code here for "relation_one" field - // type code here for "relation_many" field - // type code here for "relation_many" field - }, -]; - -// Similar logic for "relation_many" - -async function associateAnalyticWithCourse() { - const relatedCourse0 = await Courses.findOne({ - offset: Math.floor(Math.random() * (await Courses.count())), - }); - const Analytic0 = await Analytics.findOne({ - order: [['id', 'ASC']], - offset: 0, - }); - if (Analytic0?.setCourse) { - await Analytic0.setCourse(relatedCourse0); - } - - const relatedCourse1 = await Courses.findOne({ - offset: Math.floor(Math.random() * (await Courses.count())), - }); - const Analytic1 = await Analytics.findOne({ - order: [['id', 'ASC']], - offset: 1, - }); - if (Analytic1?.setCourse) { - await Analytic1.setCourse(relatedCourse1); - } - - const relatedCourse2 = await Courses.findOne({ - offset: Math.floor(Math.random() * (await Courses.count())), - }); - const Analytic2 = await Analytics.findOne({ - order: [['id', 'ASC']], - offset: 2, - }); - if (Analytic2?.setCourse) { - await Analytic2.setCourse(relatedCourse2); - } - - const relatedCourse3 = await Courses.findOne({ - offset: Math.floor(Math.random() * (await Courses.count())), - }); - const Analytic3 = await Analytics.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (Analytic3?.setCourse) { - await Analytic3.setCourse(relatedCourse3); - } - - const relatedCourse4 = await Courses.findOne({ - offset: Math.floor(Math.random() * (await Courses.count())), - }); - const Analytic4 = await Analytics.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Analytic4?.setCourse) { - await Analytic4.setCourse(relatedCourse4); - } -} - -// Similar logic for "relation_many" - -// Similar logic for "relation_many" - -// Similar logic for "relation_many" - -async function associateDiscussionBoardWithCourse() { - const relatedCourse0 = await Courses.findOne({ - offset: Math.floor(Math.random() * (await Courses.count())), - }); - const DiscussionBoard0 = await DiscussionBoards.findOne({ - order: [['id', 'ASC']], - offset: 0, - }); - if (DiscussionBoard0?.setCourse) { - await DiscussionBoard0.setCourse(relatedCourse0); - } - - const relatedCourse1 = await Courses.findOne({ - offset: Math.floor(Math.random() * (await Courses.count())), - }); - const DiscussionBoard1 = await DiscussionBoards.findOne({ - order: [['id', 'ASC']], - offset: 1, - }); - if (DiscussionBoard1?.setCourse) { - await DiscussionBoard1.setCourse(relatedCourse1); - } - - const relatedCourse2 = await Courses.findOne({ - offset: Math.floor(Math.random() * (await Courses.count())), - }); - const DiscussionBoard2 = await DiscussionBoards.findOne({ - order: [['id', 'ASC']], - offset: 2, - }); - if (DiscussionBoard2?.setCourse) { - await DiscussionBoard2.setCourse(relatedCourse2); - } - - const relatedCourse3 = await Courses.findOne({ - offset: Math.floor(Math.random() * (await Courses.count())), - }); - const DiscussionBoard3 = await DiscussionBoards.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (DiscussionBoard3?.setCourse) { - await DiscussionBoard3.setCourse(relatedCourse3); - } - - const relatedCourse4 = await Courses.findOne({ - offset: Math.floor(Math.random() * (await Courses.count())), - }); - const DiscussionBoard4 = await DiscussionBoards.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (DiscussionBoard4?.setCourse) { - await DiscussionBoard4.setCourse(relatedCourse4); - } -} - -// Similar logic for "relation_many" - -async function associateEnrollmentWithStudent() { - const relatedStudent0 = await Students.findOne({ - offset: Math.floor(Math.random() * (await Students.count())), - }); - const Enrollment0 = await Enrollments.findOne({ - order: [['id', 'ASC']], - offset: 0, - }); - if (Enrollment0?.setStudent) { - await Enrollment0.setStudent(relatedStudent0); - } - - const relatedStudent1 = await Students.findOne({ - offset: Math.floor(Math.random() * (await Students.count())), - }); - const Enrollment1 = await Enrollments.findOne({ - order: [['id', 'ASC']], - offset: 1, - }); - if (Enrollment1?.setStudent) { - await Enrollment1.setStudent(relatedStudent1); - } - - const relatedStudent2 = await Students.findOne({ - offset: Math.floor(Math.random() * (await Students.count())), - }); - const Enrollment2 = await Enrollments.findOne({ - order: [['id', 'ASC']], - offset: 2, - }); - if (Enrollment2?.setStudent) { - await Enrollment2.setStudent(relatedStudent2); - } - - const relatedStudent3 = await Students.findOne({ - offset: Math.floor(Math.random() * (await Students.count())), - }); - const Enrollment3 = await Enrollments.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (Enrollment3?.setStudent) { - await Enrollment3.setStudent(relatedStudent3); - } - - const relatedStudent4 = await Students.findOne({ - offset: Math.floor(Math.random() * (await Students.count())), - }); - const Enrollment4 = await Enrollments.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Enrollment4?.setStudent) { - await Enrollment4.setStudent(relatedStudent4); - } -} - -async function associateEnrollmentWithCourse() { - const relatedCourse0 = await Courses.findOne({ - offset: Math.floor(Math.random() * (await Courses.count())), - }); - const Enrollment0 = await Enrollments.findOne({ - order: [['id', 'ASC']], - offset: 0, - }); - if (Enrollment0?.setCourse) { - await Enrollment0.setCourse(relatedCourse0); - } - - const relatedCourse1 = await Courses.findOne({ - offset: Math.floor(Math.random() * (await Courses.count())), - }); - const Enrollment1 = await Enrollments.findOne({ - order: [['id', 'ASC']], - offset: 1, - }); - if (Enrollment1?.setCourse) { - await Enrollment1.setCourse(relatedCourse1); - } - - const relatedCourse2 = await Courses.findOne({ - offset: Math.floor(Math.random() * (await Courses.count())), - }); - const Enrollment2 = await Enrollments.findOne({ - order: [['id', 'ASC']], - offset: 2, - }); - if (Enrollment2?.setCourse) { - await Enrollment2.setCourse(relatedCourse2); - } - - const relatedCourse3 = await Courses.findOne({ - offset: Math.floor(Math.random() * (await Courses.count())), - }); - const Enrollment3 = await Enrollments.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (Enrollment3?.setCourse) { - await Enrollment3.setCourse(relatedCourse3); - } - - const relatedCourse4 = await Courses.findOne({ - offset: Math.floor(Math.random() * (await Courses.count())), - }); - const Enrollment4 = await Enrollments.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Enrollment4?.setCourse) { - await Enrollment4.setCourse(relatedCourse4); - } -} - -async function associateGradeWithStudent() { - const relatedStudent0 = await Students.findOne({ - offset: Math.floor(Math.random() * (await Students.count())), - }); - const Grade0 = await Grades.findOne({ - order: [['id', 'ASC']], - offset: 0, - }); - if (Grade0?.setStudent) { - await Grade0.setStudent(relatedStudent0); - } - - const relatedStudent1 = await Students.findOne({ - offset: Math.floor(Math.random() * (await Students.count())), - }); - const Grade1 = await Grades.findOne({ - order: [['id', 'ASC']], - offset: 1, - }); - if (Grade1?.setStudent) { - await Grade1.setStudent(relatedStudent1); - } - - const relatedStudent2 = await Students.findOne({ - offset: Math.floor(Math.random() * (await Students.count())), - }); - const Grade2 = await Grades.findOne({ - order: [['id', 'ASC']], - offset: 2, - }); - if (Grade2?.setStudent) { - await Grade2.setStudent(relatedStudent2); - } - - const relatedStudent3 = await Students.findOne({ - offset: Math.floor(Math.random() * (await Students.count())), - }); - const Grade3 = await Grades.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (Grade3?.setStudent) { - await Grade3.setStudent(relatedStudent3); - } - - const relatedStudent4 = await Students.findOne({ - offset: Math.floor(Math.random() * (await Students.count())), - }); - const Grade4 = await Grades.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Grade4?.setStudent) { - await Grade4.setStudent(relatedStudent4); - } -} - -async function associateGradeWithCourse() { - const relatedCourse0 = await Courses.findOne({ - offset: Math.floor(Math.random() * (await Courses.count())), - }); - const Grade0 = await Grades.findOne({ - order: [['id', 'ASC']], - offset: 0, - }); - if (Grade0?.setCourse) { - await Grade0.setCourse(relatedCourse0); - } - - const relatedCourse1 = await Courses.findOne({ - offset: Math.floor(Math.random() * (await Courses.count())), - }); - const Grade1 = await Grades.findOne({ - order: [['id', 'ASC']], - offset: 1, - }); - if (Grade1?.setCourse) { - await Grade1.setCourse(relatedCourse1); - } - - const relatedCourse2 = await Courses.findOne({ - offset: Math.floor(Math.random() * (await Courses.count())), - }); - const Grade2 = await Grades.findOne({ - order: [['id', 'ASC']], - offset: 2, - }); - if (Grade2?.setCourse) { - await Grade2.setCourse(relatedCourse2); - } - - const relatedCourse3 = await Courses.findOne({ - offset: Math.floor(Math.random() * (await Courses.count())), - }); - const Grade3 = await Grades.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (Grade3?.setCourse) { - await Grade3.setCourse(relatedCourse3); - } - - const relatedCourse4 = await Courses.findOne({ - offset: Math.floor(Math.random() * (await Courses.count())), - }); - const Grade4 = await Grades.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Grade4?.setCourse) { - await Grade4.setCourse(relatedCourse4); - } -} - -async function associateInstructorWithUser() { - const relatedUser0 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const Instructor0 = await Instructors.findOne({ - order: [['id', 'ASC']], - offset: 0, - }); - if (Instructor0?.setUser) { - await Instructor0.setUser(relatedUser0); - } - - const relatedUser1 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const Instructor1 = await Instructors.findOne({ - order: [['id', 'ASC']], - offset: 1, - }); - if (Instructor1?.setUser) { - await Instructor1.setUser(relatedUser1); - } - - const relatedUser2 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const Instructor2 = await Instructors.findOne({ - order: [['id', 'ASC']], - offset: 2, - }); - if (Instructor2?.setUser) { - await Instructor2.setUser(relatedUser2); - } - - const relatedUser3 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const Instructor3 = await Instructors.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (Instructor3?.setUser) { - await Instructor3.setUser(relatedUser3); - } - - const relatedUser4 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const Instructor4 = await Instructors.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Instructor4?.setUser) { - await Instructor4.setUser(relatedUser4); - } -} - -// Similar logic for "relation_many" - -async function associatePostWithDiscussion_board() { - const relatedDiscussion_board0 = await DiscussionBoards.findOne({ - offset: Math.floor(Math.random() * (await DiscussionBoards.count())), - }); - const Post0 = await Posts.findOne({ - order: [['id', 'ASC']], - offset: 0, - }); - if (Post0?.setDiscussion_board) { - await Post0.setDiscussion_board(relatedDiscussion_board0); - } - - const relatedDiscussion_board1 = await DiscussionBoards.findOne({ - offset: Math.floor(Math.random() * (await DiscussionBoards.count())), - }); - const Post1 = await Posts.findOne({ - order: [['id', 'ASC']], - offset: 1, - }); - if (Post1?.setDiscussion_board) { - await Post1.setDiscussion_board(relatedDiscussion_board1); - } - - const relatedDiscussion_board2 = await DiscussionBoards.findOne({ - offset: Math.floor(Math.random() * (await DiscussionBoards.count())), - }); - const Post2 = await Posts.findOne({ - order: [['id', 'ASC']], - offset: 2, - }); - if (Post2?.setDiscussion_board) { - await Post2.setDiscussion_board(relatedDiscussion_board2); - } - - const relatedDiscussion_board3 = await DiscussionBoards.findOne({ - offset: Math.floor(Math.random() * (await DiscussionBoards.count())), - }); - const Post3 = await Posts.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (Post3?.setDiscussion_board) { - await Post3.setDiscussion_board(relatedDiscussion_board3); - } - - const relatedDiscussion_board4 = await DiscussionBoards.findOne({ - offset: Math.floor(Math.random() * (await DiscussionBoards.count())), - }); - const Post4 = await Posts.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Post4?.setDiscussion_board) { - await Post4.setDiscussion_board(relatedDiscussion_board4); - } -} - -async function associatePostWithUser() { - const relatedUser0 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const Post0 = await Posts.findOne({ - order: [['id', 'ASC']], - offset: 0, - }); - if (Post0?.setUser) { - await Post0.setUser(relatedUser0); - } - - const relatedUser1 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const Post1 = await Posts.findOne({ - order: [['id', 'ASC']], - offset: 1, - }); - if (Post1?.setUser) { - await Post1.setUser(relatedUser1); - } - - const relatedUser2 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const Post2 = await Posts.findOne({ - order: [['id', 'ASC']], - offset: 2, - }); - if (Post2?.setUser) { - await Post2.setUser(relatedUser2); - } - - const relatedUser3 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const Post3 = await Posts.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (Post3?.setUser) { - await Post3.setUser(relatedUser3); - } - - const relatedUser4 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const Post4 = await Posts.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Post4?.setUser) { - await Post4.setUser(relatedUser4); - } -} - -async function associateStudentWithUser() { - const relatedUser0 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const Student0 = await Students.findOne({ - order: [['id', 'ASC']], - offset: 0, - }); - if (Student0?.setUser) { - await Student0.setUser(relatedUser0); - } - - const relatedUser1 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const Student1 = await Students.findOne({ - order: [['id', 'ASC']], - offset: 1, - }); - if (Student1?.setUser) { - await Student1.setUser(relatedUser1); - } - - const relatedUser2 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const Student2 = await Students.findOne({ - order: [['id', 'ASC']], - offset: 2, - }); - if (Student2?.setUser) { - await Student2.setUser(relatedUser2); - } - - const relatedUser3 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const Student3 = await Students.findOne({ - order: [['id', 'ASC']], - offset: 3, - }); - if (Student3?.setUser) { - await Student3.setUser(relatedUser3); - } - - const relatedUser4 = await Users.findOne({ - offset: Math.floor(Math.random() * (await Users.count())), - }); - const Student4 = await Students.findOne({ - order: [['id', 'ASC']], - offset: 4, - }); - if (Student4?.setUser) { - await Student4.setUser(relatedUser4); - } -} - -// Similar logic for "relation_many" +const CourseData = [{}, {}, {}, {}]; // Similar logic for "relation_many" module.exports = { up: async (queryInterface, Sequelize) => { - await Analytics.bulkCreate(AnalyticsData); + await McsPyq.bulkCreate(McsPyqData); - await Courses.bulkCreate(CoursesData); - - await DiscussionBoards.bulkCreate(DiscussionBoardsData); - - await Enrollments.bulkCreate(EnrollmentsData); - - await Grades.bulkCreate(GradesData); - - await Instructors.bulkCreate(InstructorsData); - - await Posts.bulkCreate(PostsData); - - await Students.bulkCreate(StudentsData); + await Course.bulkCreate(CourseData); await Promise.all([ // Similar logic for "relation_many" - - await associateAnalyticWithCourse(), - - // Similar logic for "relation_many" - - // Similar logic for "relation_many" - - // Similar logic for "relation_many" - - await associateDiscussionBoardWithCourse(), - - // Similar logic for "relation_many" - - await associateEnrollmentWithStudent(), - - await associateEnrollmentWithCourse(), - - await associateGradeWithStudent(), - - await associateGradeWithCourse(), - - await associateInstructorWithUser(), - - // Similar logic for "relation_many" - - await associatePostWithDiscussion_board(), - - await associatePostWithUser(), - - await associateStudentWithUser(), - - // Similar logic for "relation_many" - - // Similar logic for "relation_many" ]); }, down: async (queryInterface, Sequelize) => { - await queryInterface.bulkDelete('analytics', null, {}); + await queryInterface.bulkDelete('mcs_pyq', null, {}); - await queryInterface.bulkDelete('courses', null, {}); - - await queryInterface.bulkDelete('discussion_boards', null, {}); - - await queryInterface.bulkDelete('enrollments', null, {}); - - await queryInterface.bulkDelete('grades', null, {}); - - await queryInterface.bulkDelete('instructors', null, {}); - - await queryInterface.bulkDelete('posts', null, {}); - - await queryInterface.bulkDelete('students', null, {}); + await queryInterface.bulkDelete('course', null, {}); }, }; diff --git a/backend/src/db/seeders/20250819185634.js b/backend/src/db/seeders/20250819185634.js new file mode 100644 index 0000000..d04e813 --- /dev/null +++ b/backend/src/db/seeders/20250819185634.js @@ -0,0 +1,87 @@ +const { v4: uuid } = require('uuid'); +const db = require('../models'); +const Sequelize = require('sequelize'); +const config = require('../../config'); + +module.exports = { + /** + * @param{import("sequelize").QueryInterface} queryInterface + * @return {Promise} + */ + async up(queryInterface) { + const createdAt = new Date(); + const updatedAt = new Date(); + + /** @type {Map} */ + const idMap = new Map(); + + /** + * @param {string} key + * @return {string} + */ + function getId(key) { + if (idMap.has(key)) { + return idMap.get(key); + } + const id = uuid(); + idMap.set(key, id); + return id; + } + + /** + * @param {string} name + */ + function createPermissions(name) { + return [ + { + id: getId(`CREATE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `CREATE_${name.toUpperCase()}`, + }, + { + id: getId(`READ_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `READ_${name.toUpperCase()}`, + }, + { + id: getId(`UPDATE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `UPDATE_${name.toUpperCase()}`, + }, + { + id: getId(`DELETE_${name.toUpperCase()}`), + createdAt, + updatedAt, + name: `DELETE_${name.toUpperCase()}`, + }, + ]; + } + + const entities = ['mcs_pyq', 'course']; + + const createdPermissions = entities.flatMap(createPermissions); + + // Add permissions to database + await queryInterface.bulkInsert('permissions', createdPermissions); + // Get permissions ids + const permissionsIds = createdPermissions.map((p) => p.id); + // Get admin role + const adminRole = await db.roles.findOne({ + where: { name: config.roles.admin }, + }); + + if (adminRole) { + // Add permissions to admin role if it exists + await adminRole.addPermissions(permissionsIds); + } + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.bulkDelete( + 'permissions', + entities.flatMap(createPermissions), + ); + }, +}; diff --git a/backend/src/index.js b/backend/src/index.js index 72e476c..95fee1a 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -21,26 +21,14 @@ const contactFormRoutes = require('./routes/contactForm'); const usersRoutes = require('./routes/users'); -const analyticsRoutes = require('./routes/analytics'); - -const coursesRoutes = require('./routes/courses'); - -const discussion_boardsRoutes = require('./routes/discussion_boards'); - -const enrollmentsRoutes = require('./routes/enrollments'); - -const gradesRoutes = require('./routes/grades'); - -const instructorsRoutes = require('./routes/instructors'); - -const postsRoutes = require('./routes/posts'); - -const studentsRoutes = require('./routes/students'); - const rolesRoutes = require('./routes/roles'); const permissionsRoutes = require('./routes/permissions'); +const mcs_pyqRoutes = require('./routes/mcs_pyq'); + +const courseRoutes = require('./routes/course'); + const getBaseUrl = (url) => { if (!url) return ''; return url.endsWith('/api') ? url.slice(0, -4) : url; @@ -112,54 +100,6 @@ app.use( usersRoutes, ); -app.use( - '/api/analytics', - passport.authenticate('jwt', { session: false }), - analyticsRoutes, -); - -app.use( - '/api/courses', - passport.authenticate('jwt', { session: false }), - coursesRoutes, -); - -app.use( - '/api/discussion_boards', - passport.authenticate('jwt', { session: false }), - discussion_boardsRoutes, -); - -app.use( - '/api/enrollments', - passport.authenticate('jwt', { session: false }), - enrollmentsRoutes, -); - -app.use( - '/api/grades', - passport.authenticate('jwt', { session: false }), - gradesRoutes, -); - -app.use( - '/api/instructors', - passport.authenticate('jwt', { session: false }), - instructorsRoutes, -); - -app.use( - '/api/posts', - passport.authenticate('jwt', { session: false }), - postsRoutes, -); - -app.use( - '/api/students', - passport.authenticate('jwt', { session: false }), - studentsRoutes, -); - app.use( '/api/roles', passport.authenticate('jwt', { session: false }), @@ -172,6 +112,18 @@ app.use( permissionsRoutes, ); +app.use( + '/api/mcs_pyq', + passport.authenticate('jwt', { session: false }), + mcs_pyqRoutes, +); + +app.use( + '/api/course', + passport.authenticate('jwt', { session: false }), + courseRoutes, +); + app.use( '/api/openai', passport.authenticate('jwt', { session: false }), diff --git a/backend/src/routes/analytics.js b/backend/src/routes/analytics.js deleted file mode 100644 index 1c10e0e..0000000 --- a/backend/src/routes/analytics.js +++ /dev/null @@ -1,449 +0,0 @@ -const express = require('express'); - -const AnalyticsService = require('../services/analytics'); -const AnalyticsDBApi = require('../db/api/analytics'); -const wrapAsync = require('../helpers').wrapAsync; - -const router = express.Router(); - -const { parse } = require('json2csv'); - -const { checkCrudPermissions } = require('../middlewares/check-permissions'); - -router.use(checkCrudPermissions('analytics')); - -/** - * @swagger - * components: - * schemas: - * Analytics: - * type: object - * properties: - - * student_engagement: - * type: integer - * format: int64 - * completion_rate: - * type: integer - * format: int64 - * instructor_performance: - * type: integer - * format: int64 - - */ - -/** - * @swagger - * tags: - * name: Analytics - * description: The Analytics managing API - */ - -/** - * @swagger - * /api/analytics: - * post: - * security: - * - bearerAuth: [] - * tags: [Analytics] - * summary: Add new item - * description: Add new item - * requestBody: - * required: true - * content: - * application/json: - * schema: - * properties: - * data: - * description: Data of the updated item - * type: object - * $ref: "#/components/schemas/Analytics" - * responses: - * 200: - * description: The item was successfully added - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Analytics" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 405: - * description: Invalid input data - * 500: - * description: Some server error - */ -router.post( - '/', - wrapAsync(async (req, res) => { - const referer = - req.headers.referer || - `${req.protocol}://${req.hostname}${req.originalUrl}`; - const link = new URL(referer); - await AnalyticsService.create( - req.body.data, - req.currentUser, - true, - link.host, - ); - const payload = true; - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/budgets/bulk-import: - * post: - * security: - * - bearerAuth: [] - * tags: [Analytics] - * summary: Bulk import items - * description: Bulk import items - * requestBody: - * required: true - * content: - * application/json: - * schema: - * properties: - * data: - * description: Data of the updated items - * type: array - * items: - * $ref: "#/components/schemas/Analytics" - * responses: - * 200: - * description: The items were successfully imported - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Analytics" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 405: - * description: Invalid input data - * 500: - * description: Some server error - * - */ -router.post( - '/bulk-import', - wrapAsync(async (req, res) => { - const referer = - req.headers.referer || - `${req.protocol}://${req.hostname}${req.originalUrl}`; - const link = new URL(referer); - await AnalyticsService.bulkImport(req, res, true, link.host); - const payload = true; - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/analytics/{id}: - * put: - * security: - * - bearerAuth: [] - * tags: [Analytics] - * summary: Update the data of the selected item - * description: Update the data of the selected item - * parameters: - * - in: path - * name: id - * description: Item ID to update - * required: true - * schema: - * type: string - * requestBody: - * description: Set new item data - * required: true - * content: - * application/json: - * schema: - * properties: - * id: - * description: ID of the updated item - * type: string - * data: - * description: Data of the updated item - * type: object - * $ref: "#/components/schemas/Analytics" - * required: - * - id - * responses: - * 200: - * description: The item data was successfully updated - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Analytics" - * 400: - * description: Invalid ID supplied - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Item not found - * 500: - * description: Some server error - */ -router.put( - '/:id', - wrapAsync(async (req, res) => { - await AnalyticsService.update(req.body.data, req.body.id, req.currentUser); - const payload = true; - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/analytics/{id}: - * delete: - * security: - * - bearerAuth: [] - * tags: [Analytics] - * summary: Delete the selected item - * description: Delete the selected item - * parameters: - * - in: path - * name: id - * description: Item ID to delete - * required: true - * schema: - * type: string - * responses: - * 200: - * description: The item was successfully deleted - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Analytics" - * 400: - * description: Invalid ID supplied - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Item not found - * 500: - * description: Some server error - */ -router.delete( - '/:id', - wrapAsync(async (req, res) => { - await AnalyticsService.remove(req.params.id, req.currentUser); - const payload = true; - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/analytics/deleteByIds: - * post: - * security: - * - bearerAuth: [] - * tags: [Analytics] - * summary: Delete the selected item list - * description: Delete the selected item list - * requestBody: - * required: true - * content: - * application/json: - * schema: - * properties: - * ids: - * description: IDs of the updated items - * type: array - * responses: - * 200: - * description: The items was successfully deleted - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Analytics" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Items not found - * 500: - * description: Some server error - */ -router.post( - '/deleteByIds', - wrapAsync(async (req, res) => { - await AnalyticsService.deleteByIds(req.body.data, req.currentUser); - const payload = true; - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/analytics: - * get: - * security: - * - bearerAuth: [] - * tags: [Analytics] - * summary: Get all analytics - * description: Get all analytics - * responses: - * 200: - * description: Analytics list successfully received - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: "#/components/schemas/Analytics" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Data not found - * 500: - * description: Some server error - */ -router.get( - '/', - wrapAsync(async (req, res) => { - const filetype = req.query.filetype; - - const currentUser = req.currentUser; - const payload = await AnalyticsDBApi.findAll(req.query, { currentUser }); - if (filetype && filetype === 'csv') { - const fields = [ - 'id', - 'student_engagement', - 'completion_rate', - 'instructor_performance', - ]; - const opts = { fields }; - try { - const csv = parse(payload.rows, opts); - res.status(200).attachment(csv); - res.send(csv); - } catch (err) { - console.error(err); - } - } else { - res.status(200).send(payload); - } - }), -); - -/** - * @swagger - * /api/analytics/count: - * get: - * security: - * - bearerAuth: [] - * tags: [Analytics] - * summary: Count all analytics - * description: Count all analytics - * responses: - * 200: - * description: Analytics count successfully received - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: "#/components/schemas/Analytics" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Data not found - * 500: - * description: Some server error - */ -router.get( - '/count', - wrapAsync(async (req, res) => { - const currentUser = req.currentUser; - const payload = await AnalyticsDBApi.findAll(req.query, null, { - countOnly: true, - currentUser, - }); - - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/analytics/autocomplete: - * get: - * security: - * - bearerAuth: [] - * tags: [Analytics] - * summary: Find all analytics that match search criteria - * description: Find all analytics that match search criteria - * responses: - * 200: - * description: Analytics list successfully received - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: "#/components/schemas/Analytics" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Data not found - * 500: - * description: Some server error - */ -router.get('/autocomplete', async (req, res) => { - const payload = await AnalyticsDBApi.findAllAutocomplete( - req.query.query, - req.query.limit, - req.query.offset, - ); - - res.status(200).send(payload); -}); - -/** - * @swagger - * /api/analytics/{id}: - * get: - * security: - * - bearerAuth: [] - * tags: [Analytics] - * summary: Get selected item - * description: Get selected item - * parameters: - * - in: path - * name: id - * description: ID of item to get - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Selected item successfully received - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Analytics" - * 400: - * description: Invalid ID supplied - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Item not found - * 500: - * description: Some server error - */ -router.get( - '/:id', - wrapAsync(async (req, res) => { - const payload = await AnalyticsDBApi.findBy({ id: req.params.id }); - - res.status(200).send(payload); - }), -); - -router.use('/', require('../helpers').commonErrorHandler); - -module.exports = router; diff --git a/backend/src/routes/posts.js b/backend/src/routes/course.js similarity index 78% rename from backend/src/routes/posts.js rename to backend/src/routes/course.js index e30d2d6..d4ab98c 100644 --- a/backend/src/routes/posts.js +++ b/backend/src/routes/course.js @@ -1,7 +1,7 @@ const express = require('express'); -const PostsService = require('../services/posts'); -const PostsDBApi = require('../db/api/posts'); +const CourseService = require('../services/course'); +const CourseDBApi = require('../db/api/course'); const wrapAsync = require('../helpers').wrapAsync; const router = express.Router(); @@ -10,36 +10,32 @@ const { parse } = require('json2csv'); const { checkCrudPermissions } = require('../middlewares/check-permissions'); -router.use(checkCrudPermissions('posts')); +router.use(checkCrudPermissions('course')); /** * @swagger * components: * schemas: - * Posts: + * Course: * type: object * properties: - * content: - * type: string - * default: content - */ /** * @swagger * tags: - * name: Posts - * description: The Posts managing API + * name: Course + * description: The Course managing API */ /** * @swagger - * /api/posts: + * /api/course: * post: * security: * - bearerAuth: [] - * tags: [Posts] + * tags: [Course] * summary: Add new item * description: Add new item * requestBody: @@ -51,14 +47,14 @@ router.use(checkCrudPermissions('posts')); * data: * description: Data of the updated item * type: object - * $ref: "#/components/schemas/Posts" + * $ref: "#/components/schemas/Course" * responses: * 200: * description: The item was successfully added * content: * application/json: * schema: - * $ref: "#/components/schemas/Posts" + * $ref: "#/components/schemas/Course" * 401: * $ref: "#/components/responses/UnauthorizedError" * 405: @@ -73,7 +69,7 @@ router.post( req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; const link = new URL(referer); - await PostsService.create(req.body.data, req.currentUser, true, link.host); + await CourseService.create(req.body.data, req.currentUser, true, link.host); const payload = true; res.status(200).send(payload); }), @@ -85,7 +81,7 @@ router.post( * post: * security: * - bearerAuth: [] - * tags: [Posts] + * tags: [Course] * summary: Bulk import items * description: Bulk import items * requestBody: @@ -98,14 +94,14 @@ router.post( * description: Data of the updated items * type: array * items: - * $ref: "#/components/schemas/Posts" + * $ref: "#/components/schemas/Course" * responses: * 200: * description: The items were successfully imported * content: * application/json: * schema: - * $ref: "#/components/schemas/Posts" + * $ref: "#/components/schemas/Course" * 401: * $ref: "#/components/responses/UnauthorizedError" * 405: @@ -121,7 +117,7 @@ router.post( req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; const link = new URL(referer); - await PostsService.bulkImport(req, res, true, link.host); + await CourseService.bulkImport(req, res, true, link.host); const payload = true; res.status(200).send(payload); }), @@ -129,11 +125,11 @@ router.post( /** * @swagger - * /api/posts/{id}: + * /api/course/{id}: * put: * security: * - bearerAuth: [] - * tags: [Posts] + * tags: [Course] * summary: Update the data of the selected item * description: Update the data of the selected item * parameters: @@ -156,7 +152,7 @@ router.post( * data: * description: Data of the updated item * type: object - * $ref: "#/components/schemas/Posts" + * $ref: "#/components/schemas/Course" * required: * - id * responses: @@ -165,7 +161,7 @@ router.post( * content: * application/json: * schema: - * $ref: "#/components/schemas/Posts" + * $ref: "#/components/schemas/Course" * 400: * description: Invalid ID supplied * 401: @@ -178,7 +174,7 @@ router.post( router.put( '/:id', wrapAsync(async (req, res) => { - await PostsService.update(req.body.data, req.body.id, req.currentUser); + await CourseService.update(req.body.data, req.body.id, req.currentUser); const payload = true; res.status(200).send(payload); }), @@ -186,11 +182,11 @@ router.put( /** * @swagger - * /api/posts/{id}: + * /api/course/{id}: * delete: * security: * - bearerAuth: [] - * tags: [Posts] + * tags: [Course] * summary: Delete the selected item * description: Delete the selected item * parameters: @@ -206,7 +202,7 @@ router.put( * content: * application/json: * schema: - * $ref: "#/components/schemas/Posts" + * $ref: "#/components/schemas/Course" * 400: * description: Invalid ID supplied * 401: @@ -219,7 +215,7 @@ router.put( router.delete( '/:id', wrapAsync(async (req, res) => { - await PostsService.remove(req.params.id, req.currentUser); + await CourseService.remove(req.params.id, req.currentUser); const payload = true; res.status(200).send(payload); }), @@ -227,11 +223,11 @@ router.delete( /** * @swagger - * /api/posts/deleteByIds: + * /api/course/deleteByIds: * post: * security: * - bearerAuth: [] - * tags: [Posts] + * tags: [Course] * summary: Delete the selected item list * description: Delete the selected item list * requestBody: @@ -249,7 +245,7 @@ router.delete( * content: * application/json: * schema: - * $ref: "#/components/schemas/Posts" + * $ref: "#/components/schemas/Course" * 401: * $ref: "#/components/responses/UnauthorizedError" * 404: @@ -260,7 +256,7 @@ router.delete( router.post( '/deleteByIds', wrapAsync(async (req, res) => { - await PostsService.deleteByIds(req.body.data, req.currentUser); + await CourseService.deleteByIds(req.body.data, req.currentUser); const payload = true; res.status(200).send(payload); }), @@ -268,22 +264,22 @@ router.post( /** * @swagger - * /api/posts: + * /api/course: * get: * security: * - bearerAuth: [] - * tags: [Posts] - * summary: Get all posts - * description: Get all posts + * tags: [Course] + * summary: Get all course + * description: Get all course * responses: * 200: - * description: Posts list successfully received + * description: Course list successfully received * content: * application/json: * schema: * type: array * items: - * $ref: "#/components/schemas/Posts" + * $ref: "#/components/schemas/Course" * 401: * $ref: "#/components/responses/UnauthorizedError" * 404: @@ -297,9 +293,9 @@ router.get( const filetype = req.query.filetype; const currentUser = req.currentUser; - const payload = await PostsDBApi.findAll(req.query, { currentUser }); + const payload = await CourseDBApi.findAll(req.query, { currentUser }); if (filetype && filetype === 'csv') { - const fields = ['id', 'content', 'posted_at']; + const fields = ['id']; const opts = { fields }; try { const csv = parse(payload.rows, opts); @@ -316,22 +312,22 @@ router.get( /** * @swagger - * /api/posts/count: + * /api/course/count: * get: * security: * - bearerAuth: [] - * tags: [Posts] - * summary: Count all posts - * description: Count all posts + * tags: [Course] + * summary: Count all course + * description: Count all course * responses: * 200: - * description: Posts count successfully received + * description: Course count successfully received * content: * application/json: * schema: * type: array * items: - * $ref: "#/components/schemas/Posts" + * $ref: "#/components/schemas/Course" * 401: * $ref: "#/components/responses/UnauthorizedError" * 404: @@ -343,7 +339,7 @@ router.get( '/count', wrapAsync(async (req, res) => { const currentUser = req.currentUser; - const payload = await PostsDBApi.findAll(req.query, null, { + const payload = await CourseDBApi.findAll(req.query, null, { countOnly: true, currentUser, }); @@ -354,22 +350,22 @@ router.get( /** * @swagger - * /api/posts/autocomplete: + * /api/course/autocomplete: * get: * security: * - bearerAuth: [] - * tags: [Posts] - * summary: Find all posts that match search criteria - * description: Find all posts that match search criteria + * tags: [Course] + * summary: Find all course that match search criteria + * description: Find all course that match search criteria * responses: * 200: - * description: Posts list successfully received + * description: Course list successfully received * content: * application/json: * schema: * type: array * items: - * $ref: "#/components/schemas/Posts" + * $ref: "#/components/schemas/Course" * 401: * $ref: "#/components/responses/UnauthorizedError" * 404: @@ -378,7 +374,7 @@ router.get( * description: Some server error */ router.get('/autocomplete', async (req, res) => { - const payload = await PostsDBApi.findAllAutocomplete( + const payload = await CourseDBApi.findAllAutocomplete( req.query.query, req.query.limit, req.query.offset, @@ -389,11 +385,11 @@ router.get('/autocomplete', async (req, res) => { /** * @swagger - * /api/posts/{id}: + * /api/course/{id}: * get: * security: * - bearerAuth: [] - * tags: [Posts] + * tags: [Course] * summary: Get selected item * description: Get selected item * parameters: @@ -409,7 +405,7 @@ router.get('/autocomplete', async (req, res) => { * content: * application/json: * schema: - * $ref: "#/components/schemas/Posts" + * $ref: "#/components/schemas/Course" * 400: * description: Invalid ID supplied * 401: @@ -422,7 +418,7 @@ router.get('/autocomplete', async (req, res) => { router.get( '/:id', wrapAsync(async (req, res) => { - const payload = await PostsDBApi.findBy({ id: req.params.id }); + const payload = await CourseDBApi.findBy({ id: req.params.id }); res.status(200).send(payload); }), diff --git a/backend/src/routes/discussion_boards.js b/backend/src/routes/discussion_boards.js deleted file mode 100644 index e76e5f1..0000000 --- a/backend/src/routes/discussion_boards.js +++ /dev/null @@ -1,444 +0,0 @@ -const express = require('express'); - -const Discussion_boardsService = require('../services/discussion_boards'); -const Discussion_boardsDBApi = require('../db/api/discussion_boards'); -const wrapAsync = require('../helpers').wrapAsync; - -const router = express.Router(); - -const { parse } = require('json2csv'); - -const { checkCrudPermissions } = require('../middlewares/check-permissions'); - -router.use(checkCrudPermissions('discussion_boards')); - -/** - * @swagger - * components: - * schemas: - * Discussion_boards: - * type: object - * properties: - - * topic: - * type: string - * default: topic - - */ - -/** - * @swagger - * tags: - * name: Discussion_boards - * description: The Discussion_boards managing API - */ - -/** - * @swagger - * /api/discussion_boards: - * post: - * security: - * - bearerAuth: [] - * tags: [Discussion_boards] - * summary: Add new item - * description: Add new item - * requestBody: - * required: true - * content: - * application/json: - * schema: - * properties: - * data: - * description: Data of the updated item - * type: object - * $ref: "#/components/schemas/Discussion_boards" - * responses: - * 200: - * description: The item was successfully added - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Discussion_boards" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 405: - * description: Invalid input data - * 500: - * description: Some server error - */ -router.post( - '/', - wrapAsync(async (req, res) => { - const referer = - req.headers.referer || - `${req.protocol}://${req.hostname}${req.originalUrl}`; - const link = new URL(referer); - await Discussion_boardsService.create( - req.body.data, - req.currentUser, - true, - link.host, - ); - const payload = true; - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/budgets/bulk-import: - * post: - * security: - * - bearerAuth: [] - * tags: [Discussion_boards] - * summary: Bulk import items - * description: Bulk import items - * requestBody: - * required: true - * content: - * application/json: - * schema: - * properties: - * data: - * description: Data of the updated items - * type: array - * items: - * $ref: "#/components/schemas/Discussion_boards" - * responses: - * 200: - * description: The items were successfully imported - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Discussion_boards" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 405: - * description: Invalid input data - * 500: - * description: Some server error - * - */ -router.post( - '/bulk-import', - wrapAsync(async (req, res) => { - const referer = - req.headers.referer || - `${req.protocol}://${req.hostname}${req.originalUrl}`; - const link = new URL(referer); - await Discussion_boardsService.bulkImport(req, res, true, link.host); - const payload = true; - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/discussion_boards/{id}: - * put: - * security: - * - bearerAuth: [] - * tags: [Discussion_boards] - * summary: Update the data of the selected item - * description: Update the data of the selected item - * parameters: - * - in: path - * name: id - * description: Item ID to update - * required: true - * schema: - * type: string - * requestBody: - * description: Set new item data - * required: true - * content: - * application/json: - * schema: - * properties: - * id: - * description: ID of the updated item - * type: string - * data: - * description: Data of the updated item - * type: object - * $ref: "#/components/schemas/Discussion_boards" - * required: - * - id - * responses: - * 200: - * description: The item data was successfully updated - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Discussion_boards" - * 400: - * description: Invalid ID supplied - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Item not found - * 500: - * description: Some server error - */ -router.put( - '/:id', - wrapAsync(async (req, res) => { - await Discussion_boardsService.update( - req.body.data, - req.body.id, - req.currentUser, - ); - const payload = true; - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/discussion_boards/{id}: - * delete: - * security: - * - bearerAuth: [] - * tags: [Discussion_boards] - * summary: Delete the selected item - * description: Delete the selected item - * parameters: - * - in: path - * name: id - * description: Item ID to delete - * required: true - * schema: - * type: string - * responses: - * 200: - * description: The item was successfully deleted - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Discussion_boards" - * 400: - * description: Invalid ID supplied - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Item not found - * 500: - * description: Some server error - */ -router.delete( - '/:id', - wrapAsync(async (req, res) => { - await Discussion_boardsService.remove(req.params.id, req.currentUser); - const payload = true; - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/discussion_boards/deleteByIds: - * post: - * security: - * - bearerAuth: [] - * tags: [Discussion_boards] - * summary: Delete the selected item list - * description: Delete the selected item list - * requestBody: - * required: true - * content: - * application/json: - * schema: - * properties: - * ids: - * description: IDs of the updated items - * type: array - * responses: - * 200: - * description: The items was successfully deleted - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Discussion_boards" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Items not found - * 500: - * description: Some server error - */ -router.post( - '/deleteByIds', - wrapAsync(async (req, res) => { - await Discussion_boardsService.deleteByIds(req.body.data, req.currentUser); - const payload = true; - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/discussion_boards: - * get: - * security: - * - bearerAuth: [] - * tags: [Discussion_boards] - * summary: Get all discussion_boards - * description: Get all discussion_boards - * responses: - * 200: - * description: Discussion_boards list successfully received - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: "#/components/schemas/Discussion_boards" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Data not found - * 500: - * description: Some server error - */ -router.get( - '/', - wrapAsync(async (req, res) => { - const filetype = req.query.filetype; - - const currentUser = req.currentUser; - const payload = await Discussion_boardsDBApi.findAll(req.query, { - currentUser, - }); - if (filetype && filetype === 'csv') { - const fields = ['id', 'topic']; - const opts = { fields }; - try { - const csv = parse(payload.rows, opts); - res.status(200).attachment(csv); - res.send(csv); - } catch (err) { - console.error(err); - } - } else { - res.status(200).send(payload); - } - }), -); - -/** - * @swagger - * /api/discussion_boards/count: - * get: - * security: - * - bearerAuth: [] - * tags: [Discussion_boards] - * summary: Count all discussion_boards - * description: Count all discussion_boards - * responses: - * 200: - * description: Discussion_boards count successfully received - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: "#/components/schemas/Discussion_boards" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Data not found - * 500: - * description: Some server error - */ -router.get( - '/count', - wrapAsync(async (req, res) => { - const currentUser = req.currentUser; - const payload = await Discussion_boardsDBApi.findAll(req.query, null, { - countOnly: true, - currentUser, - }); - - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/discussion_boards/autocomplete: - * get: - * security: - * - bearerAuth: [] - * tags: [Discussion_boards] - * summary: Find all discussion_boards that match search criteria - * description: Find all discussion_boards that match search criteria - * responses: - * 200: - * description: Discussion_boards list successfully received - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: "#/components/schemas/Discussion_boards" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Data not found - * 500: - * description: Some server error - */ -router.get('/autocomplete', async (req, res) => { - const payload = await Discussion_boardsDBApi.findAllAutocomplete( - req.query.query, - req.query.limit, - req.query.offset, - ); - - res.status(200).send(payload); -}); - -/** - * @swagger - * /api/discussion_boards/{id}: - * get: - * security: - * - bearerAuth: [] - * tags: [Discussion_boards] - * summary: Get selected item - * description: Get selected item - * parameters: - * - in: path - * name: id - * description: ID of item to get - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Selected item successfully received - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Discussion_boards" - * 400: - * description: Invalid ID supplied - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Item not found - * 500: - * description: Some server error - */ -router.get( - '/:id', - wrapAsync(async (req, res) => { - const payload = await Discussion_boardsDBApi.findBy({ id: req.params.id }); - - res.status(200).send(payload); - }), -); - -router.use('/', require('../helpers').commonErrorHandler); - -module.exports = router; diff --git a/backend/src/routes/enrollments.js b/backend/src/routes/enrollments.js deleted file mode 100644 index 6881bb6..0000000 --- a/backend/src/routes/enrollments.js +++ /dev/null @@ -1,439 +0,0 @@ -const express = require('express'); - -const EnrollmentsService = require('../services/enrollments'); -const EnrollmentsDBApi = require('../db/api/enrollments'); -const wrapAsync = require('../helpers').wrapAsync; - -const router = express.Router(); - -const { parse } = require('json2csv'); - -const { checkCrudPermissions } = require('../middlewares/check-permissions'); - -router.use(checkCrudPermissions('enrollments')); - -/** - * @swagger - * components: - * schemas: - * Enrollments: - * type: object - * properties: - - * - */ - -/** - * @swagger - * tags: - * name: Enrollments - * description: The Enrollments managing API - */ - -/** - * @swagger - * /api/enrollments: - * post: - * security: - * - bearerAuth: [] - * tags: [Enrollments] - * summary: Add new item - * description: Add new item - * requestBody: - * required: true - * content: - * application/json: - * schema: - * properties: - * data: - * description: Data of the updated item - * type: object - * $ref: "#/components/schemas/Enrollments" - * responses: - * 200: - * description: The item was successfully added - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Enrollments" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 405: - * description: Invalid input data - * 500: - * description: Some server error - */ -router.post( - '/', - wrapAsync(async (req, res) => { - const referer = - req.headers.referer || - `${req.protocol}://${req.hostname}${req.originalUrl}`; - const link = new URL(referer); - await EnrollmentsService.create( - req.body.data, - req.currentUser, - true, - link.host, - ); - const payload = true; - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/budgets/bulk-import: - * post: - * security: - * - bearerAuth: [] - * tags: [Enrollments] - * summary: Bulk import items - * description: Bulk import items - * requestBody: - * required: true - * content: - * application/json: - * schema: - * properties: - * data: - * description: Data of the updated items - * type: array - * items: - * $ref: "#/components/schemas/Enrollments" - * responses: - * 200: - * description: The items were successfully imported - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Enrollments" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 405: - * description: Invalid input data - * 500: - * description: Some server error - * - */ -router.post( - '/bulk-import', - wrapAsync(async (req, res) => { - const referer = - req.headers.referer || - `${req.protocol}://${req.hostname}${req.originalUrl}`; - const link = new URL(referer); - await EnrollmentsService.bulkImport(req, res, true, link.host); - const payload = true; - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/enrollments/{id}: - * put: - * security: - * - bearerAuth: [] - * tags: [Enrollments] - * summary: Update the data of the selected item - * description: Update the data of the selected item - * parameters: - * - in: path - * name: id - * description: Item ID to update - * required: true - * schema: - * type: string - * requestBody: - * description: Set new item data - * required: true - * content: - * application/json: - * schema: - * properties: - * id: - * description: ID of the updated item - * type: string - * data: - * description: Data of the updated item - * type: object - * $ref: "#/components/schemas/Enrollments" - * required: - * - id - * responses: - * 200: - * description: The item data was successfully updated - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Enrollments" - * 400: - * description: Invalid ID supplied - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Item not found - * 500: - * description: Some server error - */ -router.put( - '/:id', - wrapAsync(async (req, res) => { - await EnrollmentsService.update( - req.body.data, - req.body.id, - req.currentUser, - ); - const payload = true; - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/enrollments/{id}: - * delete: - * security: - * - bearerAuth: [] - * tags: [Enrollments] - * summary: Delete the selected item - * description: Delete the selected item - * parameters: - * - in: path - * name: id - * description: Item ID to delete - * required: true - * schema: - * type: string - * responses: - * 200: - * description: The item was successfully deleted - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Enrollments" - * 400: - * description: Invalid ID supplied - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Item not found - * 500: - * description: Some server error - */ -router.delete( - '/:id', - wrapAsync(async (req, res) => { - await EnrollmentsService.remove(req.params.id, req.currentUser); - const payload = true; - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/enrollments/deleteByIds: - * post: - * security: - * - bearerAuth: [] - * tags: [Enrollments] - * summary: Delete the selected item list - * description: Delete the selected item list - * requestBody: - * required: true - * content: - * application/json: - * schema: - * properties: - * ids: - * description: IDs of the updated items - * type: array - * responses: - * 200: - * description: The items was successfully deleted - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Enrollments" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Items not found - * 500: - * description: Some server error - */ -router.post( - '/deleteByIds', - wrapAsync(async (req, res) => { - await EnrollmentsService.deleteByIds(req.body.data, req.currentUser); - const payload = true; - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/enrollments: - * get: - * security: - * - bearerAuth: [] - * tags: [Enrollments] - * summary: Get all enrollments - * description: Get all enrollments - * responses: - * 200: - * description: Enrollments list successfully received - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: "#/components/schemas/Enrollments" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Data not found - * 500: - * description: Some server error - */ -router.get( - '/', - wrapAsync(async (req, res) => { - const filetype = req.query.filetype; - - const currentUser = req.currentUser; - const payload = await EnrollmentsDBApi.findAll(req.query, { currentUser }); - if (filetype && filetype === 'csv') { - const fields = ['id']; - const opts = { fields }; - try { - const csv = parse(payload.rows, opts); - res.status(200).attachment(csv); - res.send(csv); - } catch (err) { - console.error(err); - } - } else { - res.status(200).send(payload); - } - }), -); - -/** - * @swagger - * /api/enrollments/count: - * get: - * security: - * - bearerAuth: [] - * tags: [Enrollments] - * summary: Count all enrollments - * description: Count all enrollments - * responses: - * 200: - * description: Enrollments count successfully received - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: "#/components/schemas/Enrollments" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Data not found - * 500: - * description: Some server error - */ -router.get( - '/count', - wrapAsync(async (req, res) => { - const currentUser = req.currentUser; - const payload = await EnrollmentsDBApi.findAll(req.query, null, { - countOnly: true, - currentUser, - }); - - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/enrollments/autocomplete: - * get: - * security: - * - bearerAuth: [] - * tags: [Enrollments] - * summary: Find all enrollments that match search criteria - * description: Find all enrollments that match search criteria - * responses: - * 200: - * description: Enrollments list successfully received - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: "#/components/schemas/Enrollments" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Data not found - * 500: - * description: Some server error - */ -router.get('/autocomplete', async (req, res) => { - const payload = await EnrollmentsDBApi.findAllAutocomplete( - req.query.query, - req.query.limit, - req.query.offset, - ); - - res.status(200).send(payload); -}); - -/** - * @swagger - * /api/enrollments/{id}: - * get: - * security: - * - bearerAuth: [] - * tags: [Enrollments] - * summary: Get selected item - * description: Get selected item - * parameters: - * - in: path - * name: id - * description: ID of item to get - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Selected item successfully received - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Enrollments" - * 400: - * description: Invalid ID supplied - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Item not found - * 500: - * description: Some server error - */ -router.get( - '/:id', - wrapAsync(async (req, res) => { - const payload = await EnrollmentsDBApi.findBy({ id: req.params.id }); - - res.status(200).send(payload); - }), -); - -router.use('/', require('../helpers').commonErrorHandler); - -module.exports = router; diff --git a/backend/src/routes/instructors.js b/backend/src/routes/instructors.js deleted file mode 100644 index 9706fca..0000000 --- a/backend/src/routes/instructors.js +++ /dev/null @@ -1,442 +0,0 @@ -const express = require('express'); - -const InstructorsService = require('../services/instructors'); -const InstructorsDBApi = require('../db/api/instructors'); -const wrapAsync = require('../helpers').wrapAsync; - -const router = express.Router(); - -const { parse } = require('json2csv'); - -const { checkCrudPermissions } = require('../middlewares/check-permissions'); - -router.use(checkCrudPermissions('instructors')); - -/** - * @swagger - * components: - * schemas: - * Instructors: - * type: object - * properties: - - * qualifications: - * type: string - * default: qualifications - - */ - -/** - * @swagger - * tags: - * name: Instructors - * description: The Instructors managing API - */ - -/** - * @swagger - * /api/instructors: - * post: - * security: - * - bearerAuth: [] - * tags: [Instructors] - * summary: Add new item - * description: Add new item - * requestBody: - * required: true - * content: - * application/json: - * schema: - * properties: - * data: - * description: Data of the updated item - * type: object - * $ref: "#/components/schemas/Instructors" - * responses: - * 200: - * description: The item was successfully added - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Instructors" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 405: - * description: Invalid input data - * 500: - * description: Some server error - */ -router.post( - '/', - wrapAsync(async (req, res) => { - const referer = - req.headers.referer || - `${req.protocol}://${req.hostname}${req.originalUrl}`; - const link = new URL(referer); - await InstructorsService.create( - req.body.data, - req.currentUser, - true, - link.host, - ); - const payload = true; - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/budgets/bulk-import: - * post: - * security: - * - bearerAuth: [] - * tags: [Instructors] - * summary: Bulk import items - * description: Bulk import items - * requestBody: - * required: true - * content: - * application/json: - * schema: - * properties: - * data: - * description: Data of the updated items - * type: array - * items: - * $ref: "#/components/schemas/Instructors" - * responses: - * 200: - * description: The items were successfully imported - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Instructors" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 405: - * description: Invalid input data - * 500: - * description: Some server error - * - */ -router.post( - '/bulk-import', - wrapAsync(async (req, res) => { - const referer = - req.headers.referer || - `${req.protocol}://${req.hostname}${req.originalUrl}`; - const link = new URL(referer); - await InstructorsService.bulkImport(req, res, true, link.host); - const payload = true; - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/instructors/{id}: - * put: - * security: - * - bearerAuth: [] - * tags: [Instructors] - * summary: Update the data of the selected item - * description: Update the data of the selected item - * parameters: - * - in: path - * name: id - * description: Item ID to update - * required: true - * schema: - * type: string - * requestBody: - * description: Set new item data - * required: true - * content: - * application/json: - * schema: - * properties: - * id: - * description: ID of the updated item - * type: string - * data: - * description: Data of the updated item - * type: object - * $ref: "#/components/schemas/Instructors" - * required: - * - id - * responses: - * 200: - * description: The item data was successfully updated - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Instructors" - * 400: - * description: Invalid ID supplied - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Item not found - * 500: - * description: Some server error - */ -router.put( - '/:id', - wrapAsync(async (req, res) => { - await InstructorsService.update( - req.body.data, - req.body.id, - req.currentUser, - ); - const payload = true; - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/instructors/{id}: - * delete: - * security: - * - bearerAuth: [] - * tags: [Instructors] - * summary: Delete the selected item - * description: Delete the selected item - * parameters: - * - in: path - * name: id - * description: Item ID to delete - * required: true - * schema: - * type: string - * responses: - * 200: - * description: The item was successfully deleted - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Instructors" - * 400: - * description: Invalid ID supplied - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Item not found - * 500: - * description: Some server error - */ -router.delete( - '/:id', - wrapAsync(async (req, res) => { - await InstructorsService.remove(req.params.id, req.currentUser); - const payload = true; - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/instructors/deleteByIds: - * post: - * security: - * - bearerAuth: [] - * tags: [Instructors] - * summary: Delete the selected item list - * description: Delete the selected item list - * requestBody: - * required: true - * content: - * application/json: - * schema: - * properties: - * ids: - * description: IDs of the updated items - * type: array - * responses: - * 200: - * description: The items was successfully deleted - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Instructors" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Items not found - * 500: - * description: Some server error - */ -router.post( - '/deleteByIds', - wrapAsync(async (req, res) => { - await InstructorsService.deleteByIds(req.body.data, req.currentUser); - const payload = true; - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/instructors: - * get: - * security: - * - bearerAuth: [] - * tags: [Instructors] - * summary: Get all instructors - * description: Get all instructors - * responses: - * 200: - * description: Instructors list successfully received - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: "#/components/schemas/Instructors" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Data not found - * 500: - * description: Some server error - */ -router.get( - '/', - wrapAsync(async (req, res) => { - const filetype = req.query.filetype; - - const currentUser = req.currentUser; - const payload = await InstructorsDBApi.findAll(req.query, { currentUser }); - if (filetype && filetype === 'csv') { - const fields = ['id', 'qualifications']; - const opts = { fields }; - try { - const csv = parse(payload.rows, opts); - res.status(200).attachment(csv); - res.send(csv); - } catch (err) { - console.error(err); - } - } else { - res.status(200).send(payload); - } - }), -); - -/** - * @swagger - * /api/instructors/count: - * get: - * security: - * - bearerAuth: [] - * tags: [Instructors] - * summary: Count all instructors - * description: Count all instructors - * responses: - * 200: - * description: Instructors count successfully received - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: "#/components/schemas/Instructors" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Data not found - * 500: - * description: Some server error - */ -router.get( - '/count', - wrapAsync(async (req, res) => { - const currentUser = req.currentUser; - const payload = await InstructorsDBApi.findAll(req.query, null, { - countOnly: true, - currentUser, - }); - - res.status(200).send(payload); - }), -); - -/** - * @swagger - * /api/instructors/autocomplete: - * get: - * security: - * - bearerAuth: [] - * tags: [Instructors] - * summary: Find all instructors that match search criteria - * description: Find all instructors that match search criteria - * responses: - * 200: - * description: Instructors list successfully received - * content: - * application/json: - * schema: - * type: array - * items: - * $ref: "#/components/schemas/Instructors" - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Data not found - * 500: - * description: Some server error - */ -router.get('/autocomplete', async (req, res) => { - const payload = await InstructorsDBApi.findAllAutocomplete( - req.query.query, - req.query.limit, - req.query.offset, - ); - - res.status(200).send(payload); -}); - -/** - * @swagger - * /api/instructors/{id}: - * get: - * security: - * - bearerAuth: [] - * tags: [Instructors] - * summary: Get selected item - * description: Get selected item - * parameters: - * - in: path - * name: id - * description: ID of item to get - * required: true - * schema: - * type: string - * responses: - * 200: - * description: Selected item successfully received - * content: - * application/json: - * schema: - * $ref: "#/components/schemas/Instructors" - * 400: - * description: Invalid ID supplied - * 401: - * $ref: "#/components/responses/UnauthorizedError" - * 404: - * description: Item not found - * 500: - * description: Some server error - */ -router.get( - '/:id', - wrapAsync(async (req, res) => { - const payload = await InstructorsDBApi.findBy({ id: req.params.id }); - - res.status(200).send(payload); - }), -); - -router.use('/', require('../helpers').commonErrorHandler); - -module.exports = router; diff --git a/backend/src/routes/courses.js b/backend/src/routes/mcs_pyq.js similarity index 77% rename from backend/src/routes/courses.js rename to backend/src/routes/mcs_pyq.js index c7af180..556600d 100644 --- a/backend/src/routes/courses.js +++ b/backend/src/routes/mcs_pyq.js @@ -1,7 +1,7 @@ const express = require('express'); -const CoursesService = require('../services/courses'); -const CoursesDBApi = require('../db/api/courses'); +const Mcs_pyqService = require('../services/mcs_pyq'); +const Mcs_pyqDBApi = require('../db/api/mcs_pyq'); const wrapAsync = require('../helpers').wrapAsync; const router = express.Router(); @@ -10,39 +10,32 @@ const { parse } = require('json2csv'); const { checkCrudPermissions } = require('../middlewares/check-permissions'); -router.use(checkCrudPermissions('courses')); +router.use(checkCrudPermissions('mcs_pyq')); /** * @swagger * components: * schemas: - * Courses: + * Mcs_pyq: * type: object * properties: - * title: - * type: string - * default: title - * description: - * type: string - * default: description - */ /** * @swagger * tags: - * name: Courses - * description: The Courses managing API + * name: Mcs_pyq + * description: The Mcs_pyq managing API */ /** * @swagger - * /api/courses: + * /api/mcs_pyq: * post: * security: * - bearerAuth: [] - * tags: [Courses] + * tags: [Mcs_pyq] * summary: Add new item * description: Add new item * requestBody: @@ -54,14 +47,14 @@ router.use(checkCrudPermissions('courses')); * data: * description: Data of the updated item * type: object - * $ref: "#/components/schemas/Courses" + * $ref: "#/components/schemas/Mcs_pyq" * responses: * 200: * description: The item was successfully added * content: * application/json: * schema: - * $ref: "#/components/schemas/Courses" + * $ref: "#/components/schemas/Mcs_pyq" * 401: * $ref: "#/components/responses/UnauthorizedError" * 405: @@ -76,7 +69,7 @@ router.post( req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; const link = new URL(referer); - await CoursesService.create( + await Mcs_pyqService.create( req.body.data, req.currentUser, true, @@ -93,7 +86,7 @@ router.post( * post: * security: * - bearerAuth: [] - * tags: [Courses] + * tags: [Mcs_pyq] * summary: Bulk import items * description: Bulk import items * requestBody: @@ -106,14 +99,14 @@ router.post( * description: Data of the updated items * type: array * items: - * $ref: "#/components/schemas/Courses" + * $ref: "#/components/schemas/Mcs_pyq" * responses: * 200: * description: The items were successfully imported * content: * application/json: * schema: - * $ref: "#/components/schemas/Courses" + * $ref: "#/components/schemas/Mcs_pyq" * 401: * $ref: "#/components/responses/UnauthorizedError" * 405: @@ -129,7 +122,7 @@ router.post( req.headers.referer || `${req.protocol}://${req.hostname}${req.originalUrl}`; const link = new URL(referer); - await CoursesService.bulkImport(req, res, true, link.host); + await Mcs_pyqService.bulkImport(req, res, true, link.host); const payload = true; res.status(200).send(payload); }), @@ -137,11 +130,11 @@ router.post( /** * @swagger - * /api/courses/{id}: + * /api/mcs_pyq/{id}: * put: * security: * - bearerAuth: [] - * tags: [Courses] + * tags: [Mcs_pyq] * summary: Update the data of the selected item * description: Update the data of the selected item * parameters: @@ -164,7 +157,7 @@ router.post( * data: * description: Data of the updated item * type: object - * $ref: "#/components/schemas/Courses" + * $ref: "#/components/schemas/Mcs_pyq" * required: * - id * responses: @@ -173,7 +166,7 @@ router.post( * content: * application/json: * schema: - * $ref: "#/components/schemas/Courses" + * $ref: "#/components/schemas/Mcs_pyq" * 400: * description: Invalid ID supplied * 401: @@ -186,7 +179,7 @@ router.post( router.put( '/:id', wrapAsync(async (req, res) => { - await CoursesService.update(req.body.data, req.body.id, req.currentUser); + await Mcs_pyqService.update(req.body.data, req.body.id, req.currentUser); const payload = true; res.status(200).send(payload); }), @@ -194,11 +187,11 @@ router.put( /** * @swagger - * /api/courses/{id}: + * /api/mcs_pyq/{id}: * delete: * security: * - bearerAuth: [] - * tags: [Courses] + * tags: [Mcs_pyq] * summary: Delete the selected item * description: Delete the selected item * parameters: @@ -214,7 +207,7 @@ router.put( * content: * application/json: * schema: - * $ref: "#/components/schemas/Courses" + * $ref: "#/components/schemas/Mcs_pyq" * 400: * description: Invalid ID supplied * 401: @@ -227,7 +220,7 @@ router.put( router.delete( '/:id', wrapAsync(async (req, res) => { - await CoursesService.remove(req.params.id, req.currentUser); + await Mcs_pyqService.remove(req.params.id, req.currentUser); const payload = true; res.status(200).send(payload); }), @@ -235,11 +228,11 @@ router.delete( /** * @swagger - * /api/courses/deleteByIds: + * /api/mcs_pyq/deleteByIds: * post: * security: * - bearerAuth: [] - * tags: [Courses] + * tags: [Mcs_pyq] * summary: Delete the selected item list * description: Delete the selected item list * requestBody: @@ -257,7 +250,7 @@ router.delete( * content: * application/json: * schema: - * $ref: "#/components/schemas/Courses" + * $ref: "#/components/schemas/Mcs_pyq" * 401: * $ref: "#/components/responses/UnauthorizedError" * 404: @@ -268,7 +261,7 @@ router.delete( router.post( '/deleteByIds', wrapAsync(async (req, res) => { - await CoursesService.deleteByIds(req.body.data, req.currentUser); + await Mcs_pyqService.deleteByIds(req.body.data, req.currentUser); const payload = true; res.status(200).send(payload); }), @@ -276,22 +269,22 @@ router.post( /** * @swagger - * /api/courses: + * /api/mcs_pyq: * get: * security: * - bearerAuth: [] - * tags: [Courses] - * summary: Get all courses - * description: Get all courses + * tags: [Mcs_pyq] + * summary: Get all mcs_pyq + * description: Get all mcs_pyq * responses: * 200: - * description: Courses list successfully received + * description: Mcs_pyq list successfully received * content: * application/json: * schema: * type: array * items: - * $ref: "#/components/schemas/Courses" + * $ref: "#/components/schemas/Mcs_pyq" * 401: * $ref: "#/components/responses/UnauthorizedError" * 404: @@ -305,9 +298,9 @@ router.get( const filetype = req.query.filetype; const currentUser = req.currentUser; - const payload = await CoursesDBApi.findAll(req.query, { currentUser }); + const payload = await Mcs_pyqDBApi.findAll(req.query, { currentUser }); if (filetype && filetype === 'csv') { - const fields = ['id', 'title', 'description']; + const fields = ['id']; const opts = { fields }; try { const csv = parse(payload.rows, opts); @@ -324,22 +317,22 @@ router.get( /** * @swagger - * /api/courses/count: + * /api/mcs_pyq/count: * get: * security: * - bearerAuth: [] - * tags: [Courses] - * summary: Count all courses - * description: Count all courses + * tags: [Mcs_pyq] + * summary: Count all mcs_pyq + * description: Count all mcs_pyq * responses: * 200: - * description: Courses count successfully received + * description: Mcs_pyq count successfully received * content: * application/json: * schema: * type: array * items: - * $ref: "#/components/schemas/Courses" + * $ref: "#/components/schemas/Mcs_pyq" * 401: * $ref: "#/components/responses/UnauthorizedError" * 404: @@ -351,7 +344,7 @@ router.get( '/count', wrapAsync(async (req, res) => { const currentUser = req.currentUser; - const payload = await CoursesDBApi.findAll(req.query, null, { + const payload = await Mcs_pyqDBApi.findAll(req.query, null, { countOnly: true, currentUser, }); @@ -362,22 +355,22 @@ router.get( /** * @swagger - * /api/courses/autocomplete: + * /api/mcs_pyq/autocomplete: * get: * security: * - bearerAuth: [] - * tags: [Courses] - * summary: Find all courses that match search criteria - * description: Find all courses that match search criteria + * tags: [Mcs_pyq] + * summary: Find all mcs_pyq that match search criteria + * description: Find all mcs_pyq that match search criteria * responses: * 200: - * description: Courses list successfully received + * description: Mcs_pyq list successfully received * content: * application/json: * schema: * type: array * items: - * $ref: "#/components/schemas/Courses" + * $ref: "#/components/schemas/Mcs_pyq" * 401: * $ref: "#/components/responses/UnauthorizedError" * 404: @@ -386,7 +379,7 @@ router.get( * description: Some server error */ router.get('/autocomplete', async (req, res) => { - const payload = await CoursesDBApi.findAllAutocomplete( + const payload = await Mcs_pyqDBApi.findAllAutocomplete( req.query.query, req.query.limit, req.query.offset, @@ -397,11 +390,11 @@ router.get('/autocomplete', async (req, res) => { /** * @swagger - * /api/courses/{id}: + * /api/mcs_pyq/{id}: * get: * security: * - bearerAuth: [] - * tags: [Courses] + * tags: [Mcs_pyq] * summary: Get selected item * description: Get selected item * parameters: @@ -417,7 +410,7 @@ router.get('/autocomplete', async (req, res) => { * content: * application/json: * schema: - * $ref: "#/components/schemas/Courses" + * $ref: "#/components/schemas/Mcs_pyq" * 400: * description: Invalid ID supplied * 401: @@ -430,7 +423,7 @@ router.get('/autocomplete', async (req, res) => { router.get( '/:id', wrapAsync(async (req, res) => { - const payload = await CoursesDBApi.findBy({ id: req.params.id }); + const payload = await Mcs_pyqDBApi.findBy({ id: req.params.id }); res.status(200).send(payload); }), diff --git a/backend/src/services/analytics.js b/backend/src/services/analytics.js deleted file mode 100644 index c9244c2..0000000 --- a/backend/src/services/analytics.js +++ /dev/null @@ -1,114 +0,0 @@ -const db = require('../db/models'); -const AnalyticsDBApi = require('../db/api/analytics'); -const processFile = require('../middlewares/upload'); -const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); -const stream = require('stream'); - -module.exports = class AnalyticsService { - static async create(data, currentUser) { - const transaction = await db.sequelize.transaction(); - try { - await AnalyticsDBApi.create(data, { - currentUser, - transaction, - }); - - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - } - - static async bulkImport(req, res, sendInvitationEmails = true, host) { - const transaction = await db.sequelize.transaction(); - - try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }); - - await AnalyticsDBApi.bulkImport(results, { - transaction, - ignoreDuplicates: true, - validate: true, - currentUser: req.currentUser, - }); - - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - } - - static async update(data, id, currentUser) { - const transaction = await db.sequelize.transaction(); - try { - let analytics = await AnalyticsDBApi.findBy({ id }, { transaction }); - - if (!analytics) { - throw new ValidationError('analyticsNotFound'); - } - - const updatedAnalytics = await AnalyticsDBApi.update(id, data, { - currentUser, - transaction, - }); - - await transaction.commit(); - return updatedAnalytics; - } catch (error) { - await transaction.rollback(); - throw error; - } - } - - static async deleteByIds(ids, currentUser) { - const transaction = await db.sequelize.transaction(); - - try { - await AnalyticsDBApi.deleteByIds(ids, { - currentUser, - transaction, - }); - - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - } - - static async remove(id, currentUser) { - const transaction = await db.sequelize.transaction(); - - try { - await AnalyticsDBApi.remove(id, { - currentUser, - transaction, - }); - - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - } -}; diff --git a/backend/src/services/posts.js b/backend/src/services/course.js similarity index 82% rename from backend/src/services/posts.js rename to backend/src/services/course.js index 18bba46..aa30920 100644 --- a/backend/src/services/posts.js +++ b/backend/src/services/course.js @@ -1,5 +1,5 @@ const db = require('../db/models'); -const PostsDBApi = require('../db/api/posts'); +const CourseDBApi = require('../db/api/course'); const processFile = require('../middlewares/upload'); const ValidationError = require('./notifications/errors/validation'); const csv = require('csv-parser'); @@ -7,11 +7,11 @@ const axios = require('axios'); const config = require('../config'); const stream = require('stream'); -module.exports = class PostsService { +module.exports = class CourseService { static async create(data, currentUser) { const transaction = await db.sequelize.transaction(); try { - await PostsDBApi.create(data, { + await CourseDBApi.create(data, { currentUser, transaction, }); @@ -44,7 +44,7 @@ module.exports = class PostsService { .on('error', (error) => reject(error)); }); - await PostsDBApi.bulkImport(results, { + await CourseDBApi.bulkImport(results, { transaction, ignoreDuplicates: true, validate: true, @@ -61,19 +61,19 @@ module.exports = class PostsService { static async update(data, id, currentUser) { const transaction = await db.sequelize.transaction(); try { - let posts = await PostsDBApi.findBy({ id }, { transaction }); + let course = await CourseDBApi.findBy({ id }, { transaction }); - if (!posts) { - throw new ValidationError('postsNotFound'); + if (!course) { + throw new ValidationError('courseNotFound'); } - const updatedPosts = await PostsDBApi.update(id, data, { + const updatedCourse = await CourseDBApi.update(id, data, { currentUser, transaction, }); await transaction.commit(); - return updatedPosts; + return updatedCourse; } catch (error) { await transaction.rollback(); throw error; @@ -84,7 +84,7 @@ module.exports = class PostsService { const transaction = await db.sequelize.transaction(); try { - await PostsDBApi.deleteByIds(ids, { + await CourseDBApi.deleteByIds(ids, { currentUser, transaction, }); @@ -100,7 +100,7 @@ module.exports = class PostsService { const transaction = await db.sequelize.transaction(); try { - await PostsDBApi.remove(id, { + await CourseDBApi.remove(id, { currentUser, transaction, }); diff --git a/backend/src/services/discussion_boards.js b/backend/src/services/discussion_boards.js deleted file mode 100644 index 7e23bd3..0000000 --- a/backend/src/services/discussion_boards.js +++ /dev/null @@ -1,121 +0,0 @@ -const db = require('../db/models'); -const Discussion_boardsDBApi = require('../db/api/discussion_boards'); -const processFile = require('../middlewares/upload'); -const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); -const stream = require('stream'); - -module.exports = class Discussion_boardsService { - static async create(data, currentUser) { - const transaction = await db.sequelize.transaction(); - try { - await Discussion_boardsDBApi.create(data, { - currentUser, - transaction, - }); - - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - } - - static async bulkImport(req, res, sendInvitationEmails = true, host) { - const transaction = await db.sequelize.transaction(); - - try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }); - - await Discussion_boardsDBApi.bulkImport(results, { - transaction, - ignoreDuplicates: true, - validate: true, - currentUser: req.currentUser, - }); - - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - } - - static async update(data, id, currentUser) { - const transaction = await db.sequelize.transaction(); - try { - let discussion_boards = await Discussion_boardsDBApi.findBy( - { id }, - { transaction }, - ); - - if (!discussion_boards) { - throw new ValidationError('discussion_boardsNotFound'); - } - - const updatedDiscussion_boards = await Discussion_boardsDBApi.update( - id, - data, - { - currentUser, - transaction, - }, - ); - - await transaction.commit(); - return updatedDiscussion_boards; - } catch (error) { - await transaction.rollback(); - throw error; - } - } - - static async deleteByIds(ids, currentUser) { - const transaction = await db.sequelize.transaction(); - - try { - await Discussion_boardsDBApi.deleteByIds(ids, { - currentUser, - transaction, - }); - - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - } - - static async remove(id, currentUser) { - const transaction = await db.sequelize.transaction(); - - try { - await Discussion_boardsDBApi.remove(id, { - currentUser, - transaction, - }); - - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - } -}; diff --git a/backend/src/services/enrollments.js b/backend/src/services/enrollments.js deleted file mode 100644 index 83d3258..0000000 --- a/backend/src/services/enrollments.js +++ /dev/null @@ -1,114 +0,0 @@ -const db = require('../db/models'); -const EnrollmentsDBApi = require('../db/api/enrollments'); -const processFile = require('../middlewares/upload'); -const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); -const stream = require('stream'); - -module.exports = class EnrollmentsService { - static async create(data, currentUser) { - const transaction = await db.sequelize.transaction(); - try { - await EnrollmentsDBApi.create(data, { - currentUser, - transaction, - }); - - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - } - - static async bulkImport(req, res, sendInvitationEmails = true, host) { - const transaction = await db.sequelize.transaction(); - - try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }); - - await EnrollmentsDBApi.bulkImport(results, { - transaction, - ignoreDuplicates: true, - validate: true, - currentUser: req.currentUser, - }); - - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - } - - static async update(data, id, currentUser) { - const transaction = await db.sequelize.transaction(); - try { - let enrollments = await EnrollmentsDBApi.findBy({ id }, { transaction }); - - if (!enrollments) { - throw new ValidationError('enrollmentsNotFound'); - } - - const updatedEnrollments = await EnrollmentsDBApi.update(id, data, { - currentUser, - transaction, - }); - - await transaction.commit(); - return updatedEnrollments; - } catch (error) { - await transaction.rollback(); - throw error; - } - } - - static async deleteByIds(ids, currentUser) { - const transaction = await db.sequelize.transaction(); - - try { - await EnrollmentsDBApi.deleteByIds(ids, { - currentUser, - transaction, - }); - - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - } - - static async remove(id, currentUser) { - const transaction = await db.sequelize.transaction(); - - try { - await EnrollmentsDBApi.remove(id, { - currentUser, - transaction, - }); - - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - } -}; diff --git a/backend/src/services/instructors.js b/backend/src/services/instructors.js deleted file mode 100644 index 5677144..0000000 --- a/backend/src/services/instructors.js +++ /dev/null @@ -1,114 +0,0 @@ -const db = require('../db/models'); -const InstructorsDBApi = require('../db/api/instructors'); -const processFile = require('../middlewares/upload'); -const ValidationError = require('./notifications/errors/validation'); -const csv = require('csv-parser'); -const axios = require('axios'); -const config = require('../config'); -const stream = require('stream'); - -module.exports = class InstructorsService { - static async create(data, currentUser) { - const transaction = await db.sequelize.transaction(); - try { - await InstructorsDBApi.create(data, { - currentUser, - transaction, - }); - - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - } - - static async bulkImport(req, res, sendInvitationEmails = true, host) { - const transaction = await db.sequelize.transaction(); - - try { - await processFile(req, res); - const bufferStream = new stream.PassThrough(); - const results = []; - - await bufferStream.end(Buffer.from(req.file.buffer, 'utf-8')); // convert Buffer to Stream - - await new Promise((resolve, reject) => { - bufferStream - .pipe(csv()) - .on('data', (data) => results.push(data)) - .on('end', async () => { - console.log('CSV results', results); - resolve(); - }) - .on('error', (error) => reject(error)); - }); - - await InstructorsDBApi.bulkImport(results, { - transaction, - ignoreDuplicates: true, - validate: true, - currentUser: req.currentUser, - }); - - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - } - - static async update(data, id, currentUser) { - const transaction = await db.sequelize.transaction(); - try { - let instructors = await InstructorsDBApi.findBy({ id }, { transaction }); - - if (!instructors) { - throw new ValidationError('instructorsNotFound'); - } - - const updatedInstructors = await InstructorsDBApi.update(id, data, { - currentUser, - transaction, - }); - - await transaction.commit(); - return updatedInstructors; - } catch (error) { - await transaction.rollback(); - throw error; - } - } - - static async deleteByIds(ids, currentUser) { - const transaction = await db.sequelize.transaction(); - - try { - await InstructorsDBApi.deleteByIds(ids, { - currentUser, - transaction, - }); - - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - } - - static async remove(id, currentUser) { - const transaction = await db.sequelize.transaction(); - - try { - await InstructorsDBApi.remove(id, { - currentUser, - transaction, - }); - - await transaction.commit(); - } catch (error) { - await transaction.rollback(); - throw error; - } - } -}; diff --git a/backend/src/services/students.js b/backend/src/services/mcs_pyq.js similarity index 82% rename from backend/src/services/students.js rename to backend/src/services/mcs_pyq.js index 108c68c..50d647b 100644 --- a/backend/src/services/students.js +++ b/backend/src/services/mcs_pyq.js @@ -1,5 +1,5 @@ const db = require('../db/models'); -const StudentsDBApi = require('../db/api/students'); +const Mcs_pyqDBApi = require('../db/api/mcs_pyq'); const processFile = require('../middlewares/upload'); const ValidationError = require('./notifications/errors/validation'); const csv = require('csv-parser'); @@ -7,11 +7,11 @@ const axios = require('axios'); const config = require('../config'); const stream = require('stream'); -module.exports = class StudentsService { +module.exports = class Mcs_pyqService { static async create(data, currentUser) { const transaction = await db.sequelize.transaction(); try { - await StudentsDBApi.create(data, { + await Mcs_pyqDBApi.create(data, { currentUser, transaction, }); @@ -44,7 +44,7 @@ module.exports = class StudentsService { .on('error', (error) => reject(error)); }); - await StudentsDBApi.bulkImport(results, { + await Mcs_pyqDBApi.bulkImport(results, { transaction, ignoreDuplicates: true, validate: true, @@ -61,19 +61,19 @@ module.exports = class StudentsService { static async update(data, id, currentUser) { const transaction = await db.sequelize.transaction(); try { - let students = await StudentsDBApi.findBy({ id }, { transaction }); + let mcs_pyq = await Mcs_pyqDBApi.findBy({ id }, { transaction }); - if (!students) { - throw new ValidationError('studentsNotFound'); + if (!mcs_pyq) { + throw new ValidationError('mcs_pyqNotFound'); } - const updatedStudents = await StudentsDBApi.update(id, data, { + const updatedMcs_pyq = await Mcs_pyqDBApi.update(id, data, { currentUser, transaction, }); await transaction.commit(); - return updatedStudents; + return updatedMcs_pyq; } catch (error) { await transaction.rollback(); throw error; @@ -84,7 +84,7 @@ module.exports = class StudentsService { const transaction = await db.sequelize.transaction(); try { - await StudentsDBApi.deleteByIds(ids, { + await Mcs_pyqDBApi.deleteByIds(ids, { currentUser, transaction, }); @@ -100,7 +100,7 @@ module.exports = class StudentsService { const transaction = await db.sequelize.transaction(); try { - await StudentsDBApi.remove(id, { + await Mcs_pyqDBApi.remove(id, { currentUser, transaction, }); diff --git a/backend/src/services/search.js b/backend/src/services/search.js index 797a16d..e37e9f6 100644 --- a/backend/src/services/search.js +++ b/backend/src/services/search.js @@ -42,26 +42,8 @@ module.exports = class SearchService { } const tableColumns = { users: ['firstName', 'lastName', 'phoneNumber', 'email'], - - courses: ['title', 'description'], - - discussion_boards: ['topic'], - - instructors: ['qualifications'], - - posts: ['content'], - }; - const columnsInt = { - analytics: [ - 'student_engagement', - - 'completion_rate', - - 'instructor_performance', - ], - - grades: ['grade'], }; + const columnsInt = {}; let allFoundRecords = []; diff --git a/frontend/json/runtimeError.json b/frontend/json/runtimeError.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/frontend/json/runtimeError.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/frontend/src/components/Analytics/CardAnalytics.tsx b/frontend/src/components/Analytics/CardAnalytics.tsx deleted file mode 100644 index de9d861..0000000 --- a/frontend/src/components/Analytics/CardAnalytics.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import React from 'react'; -import ImageField from '../ImageField'; -import ListActionsPopover from '../ListActionsPopover'; -import { useAppSelector } from '../../stores/hooks'; -import dataFormatter from '../../helpers/dataFormatter'; -import { Pagination } from '../Pagination'; -import { saveFile } from '../../helpers/fileSaver'; -import LoadingSpinner from '../LoadingSpinner'; -import Link from 'next/link'; - -import { hasPermission } from '../../helpers/userPermissions'; - -type Props = { - analytics: any[]; - loading: boolean; - onDelete: (id: string) => void; - currentPage: number; - numPages: number; - onPageChange: (page: number) => void; -}; - -const CardAnalytics = ({ - analytics, - loading, - onDelete, - currentPage, - numPages, - onPageChange, -}: Props) => { - const asideScrollbarsStyle = useAppSelector( - (state) => state.style.asideScrollbarsStyle, - ); - const bgColor = useAppSelector((state) => state.style.cardsColor); - const darkMode = useAppSelector((state) => state.style.darkMode); - const corners = useAppSelector((state) => state.style.corners); - const focusRing = useAppSelector((state) => state.style.focusRingColor); - - const currentUser = useAppSelector((state) => state.auth.currentUser); - const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_ANALYTICS'); - - return ( -
- {loading && } -
    - {!loading && - analytics.map((item, index) => ( -
  • -
    - - {item.course} - - -
    - -
    -
    -
    -
    -
    - Course -
    -
    -
    - {dataFormatter.coursesOneListFormatter(item.course)} -
    -
    -
    - -
    -
    - StudentEngagement -
    -
    -
    - {item.student_engagement} -
    -
    -
    - -
    -
    - CompletionRate -
    -
    -
    - {item.completion_rate} -
    -
    -
    - -
    -
    - InstructorPerformance -
    -
    -
    - {item.instructor_performance} -
    -
    -
    -
    -
  • - ))} - {!loading && analytics.length === 0 && ( -
    -

    No data to display

    -
    - )} -
-
- -
-
- ); -}; - -export default CardAnalytics; diff --git a/frontend/src/components/Analytics/ListAnalytics.tsx b/frontend/src/components/Analytics/ListAnalytics.tsx deleted file mode 100644 index 9180dfc..0000000 --- a/frontend/src/components/Analytics/ListAnalytics.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import React from 'react'; -import CardBox from '../CardBox'; -import ImageField from '../ImageField'; -import dataFormatter from '../../helpers/dataFormatter'; -import { saveFile } from '../../helpers/fileSaver'; -import ListActionsPopover from '../ListActionsPopover'; -import { useAppSelector } from '../../stores/hooks'; -import { Pagination } from '../Pagination'; -import LoadingSpinner from '../LoadingSpinner'; -import Link from 'next/link'; - -import { hasPermission } from '../../helpers/userPermissions'; - -type Props = { - analytics: any[]; - loading: boolean; - onDelete: (id: string) => void; - currentPage: number; - numPages: number; - onPageChange: (page: number) => void; -}; - -const ListAnalytics = ({ - analytics, - loading, - onDelete, - currentPage, - numPages, - onPageChange, -}: Props) => { - const currentUser = useAppSelector((state) => state.auth.currentUser); - const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_ANALYTICS'); - - const corners = useAppSelector((state) => state.style.corners); - const bgColor = useAppSelector((state) => state.style.cardsColor); - - return ( - <> -
- {loading && } - {!loading && - analytics.map((item) => ( -
- -
- dark:divide-dark-700 overflow-x-auto' - } - > -
-

Course

-

- {dataFormatter.coursesOneListFormatter(item.course)} -

-
- -
-

- StudentEngagement -

-

- {item.student_engagement} -

-
- -
-

- CompletionRate -

-

{item.completion_rate}

-
- -
-

- InstructorPerformance -

-

- {item.instructor_performance} -

-
- - -
-
-
- ))} - {!loading && analytics.length === 0 && ( -
-

No data to display

-
- )} -
-
- -
- - ); -}; - -export default ListAnalytics; diff --git a/frontend/src/components/Analytics/configureAnalyticsCols.tsx b/frontend/src/components/Analytics/configureAnalyticsCols.tsx deleted file mode 100644 index 488d7fc..0000000 --- a/frontend/src/components/Analytics/configureAnalyticsCols.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import React from 'react'; -import BaseIcon from '../BaseIcon'; -import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; -import axios from 'axios'; -import { - GridActionsCellItem, - GridRowParams, - GridValueGetterParams, -} from '@mui/x-data-grid'; -import ImageField from '../ImageField'; -import { saveFile } from '../../helpers/fileSaver'; -import dataFormatter from '../../helpers/dataFormatter'; -import DataGridMultiSelect from '../DataGridMultiSelect'; -import ListActionsPopover from '../ListActionsPopover'; - -import { hasPermission } from '../../helpers/userPermissions'; - -type Params = (id: string) => void; - -export const loadColumns = async ( - onDelete: Params, - entityName: string, - - user, -) => { - async function callOptionsApi(entityName: string) { - if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; - - try { - const data = await axios(`/${entityName}/autocomplete?limit=100`); - return data.data; - } catch (error) { - console.log(error); - return []; - } - } - - const hasUpdatePermission = hasPermission(user, 'UPDATE_ANALYTICS'); - - return [ - { - field: 'course', - headerName: 'Course', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - sortable: false, - type: 'singleSelect', - getOptionValue: (value: any) => value?.id, - getOptionLabel: (value: any) => value?.label, - valueOptions: await callOptionsApi('courses'), - valueGetter: (params: GridValueGetterParams) => - params?.value?.id ?? params?.value, - }, - - { - field: 'student_engagement', - headerName: 'StudentEngagement', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - type: 'number', - }, - - { - field: 'completion_rate', - headerName: 'CompletionRate', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - type: 'number', - }, - - { - field: 'instructor_performance', - headerName: 'InstructorPerformance', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - type: 'number', - }, - - { - field: 'actions', - type: 'actions', - minWidth: 30, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - getActions: (params: GridRowParams) => { - return [ -
- -
, - ]; - }, - }, - ]; -}; diff --git a/frontend/src/components/Instructors/CardInstructors.tsx b/frontend/src/components/Course/CardCourse.tsx similarity index 61% rename from frontend/src/components/Instructors/CardInstructors.tsx rename to frontend/src/components/Course/CardCourse.tsx index fb6e5f4..85d7168 100644 --- a/frontend/src/components/Instructors/CardInstructors.tsx +++ b/frontend/src/components/Course/CardCourse.tsx @@ -11,7 +11,7 @@ import Link from 'next/link'; import { hasPermission } from '../../helpers/userPermissions'; type Props = { - instructors: any[]; + course: any[]; loading: boolean; onDelete: (id: string) => void; currentPage: number; @@ -19,8 +19,8 @@ type Props = { onPageChange: (page: number) => void; }; -const CardInstructors = ({ - instructors, +const CardCourse = ({ + course, loading, onDelete, currentPage, @@ -36,7 +36,7 @@ const CardInstructors = ({ const focusRing = useAppSelector((state) => state.style.focusRingColor); const currentUser = useAppSelector((state) => state.auth.currentUser); - const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_INSTRUCTORS'); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_COURSE'); return (
@@ -46,7 +46,7 @@ const CardInstructors = ({ className='grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-3 2xl:grid-cols-4 xl:gap-x-8' > {!loading && - instructors.map((item, index) => ( + course.map((item, index) => (
  • - {item.user} + {item.id}
  • -
    -
    -
    User
    -
    -
    - {dataFormatter.usersOneListFormatter(item.user)} -
    -
    -
    - -
    -
    - Qualifications -
    -
    -
    - {item.qualifications} -
    -
    -
    - -
    -
    - Courses -
    -
    -
    - {dataFormatter - .coursesManyListFormatter(item.courses) - .join(', ')} -
    -
    -
    -
    +
    ))} - {!loading && instructors.length === 0 && ( + {!loading && course.length === 0 && (

    No data to display

    @@ -128,4 +95,4 @@ const CardInstructors = ({ ); }; -export default CardInstructors; +export default CardCourse; diff --git a/frontend/src/components/Instructors/ListInstructors.tsx b/frontend/src/components/Course/ListCourse.tsx similarity index 62% rename from frontend/src/components/Instructors/ListInstructors.tsx rename to frontend/src/components/Course/ListCourse.tsx index 0335344..50cc078 100644 --- a/frontend/src/components/Instructors/ListInstructors.tsx +++ b/frontend/src/components/Course/ListCourse.tsx @@ -12,7 +12,7 @@ import Link from 'next/link'; import { hasPermission } from '../../helpers/userPermissions'; type Props = { - instructors: any[]; + course: any[]; loading: boolean; onDelete: (id: string) => void; currentPage: number; @@ -20,8 +20,8 @@ type Props = { onPageChange: (page: number) => void; }; -const ListInstructors = ({ - instructors, +const ListCourse = ({ + course, loading, onDelete, currentPage, @@ -29,7 +29,7 @@ const ListInstructors = ({ onPageChange, }: Props) => { const currentUser = useAppSelector((state) => state.auth.currentUser); - const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_INSTRUCTORS'); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_COURSE'); const corners = useAppSelector((state) => state.style.corners); const bgColor = useAppSelector((state) => state.style.cardsColor); @@ -39,7 +39,7 @@ const ListInstructors = ({
    {loading && } {!loading && - instructors.map((item) => ( + course.map((item) => (
    dark:divide-dark-700 overflow-x-auto' } - > -
    -

    User

    -

    - {dataFormatter.usersOneListFormatter(item.user)} -

    -
    - -
    -

    - Qualifications -

    -

    {item.qualifications}

    -
    - -
    -

    Courses

    -

    - {dataFormatter - .coursesManyListFormatter(item.courses) - .join(', ')} -

    -
    - + >
    ))} - {!loading && instructors.length === 0 && ( + {!loading && course.length === 0 && (

    No data to display

    @@ -104,4 +81,4 @@ const ListInstructors = ({ ); }; -export default ListInstructors; +export default ListCourse; diff --git a/frontend/src/components/Enrollments/TableEnrollments.tsx b/frontend/src/components/Course/TableCourse.tsx similarity index 96% rename from frontend/src/components/Enrollments/TableEnrollments.tsx rename to frontend/src/components/Course/TableCourse.tsx index b29a7de..a248b8f 100644 --- a/frontend/src/components/Enrollments/TableEnrollments.tsx +++ b/frontend/src/components/Course/TableCourse.tsx @@ -10,19 +10,19 @@ import { deleteItem, setRefetch, deleteItemsByIds, -} from '../../stores/enrollments/enrollmentsSlice'; +} from '../../stores/course/courseSlice'; import { useAppDispatch, useAppSelector } from '../../stores/hooks'; import { useRouter } from 'next/router'; import { Field, Form, Formik } from 'formik'; import { DataGrid, GridColDef } from '@mui/x-data-grid'; -import { loadColumns } from './configureEnrollmentsCols'; +import { loadColumns } from './configureCourseCols'; import _ from 'lodash'; import dataFormatter from '../../helpers/dataFormatter'; import { dataGridStyles } from '../../styles'; const perPage = 10; -const TableSampleEnrollments = ({ +const TableSampleCourse = ({ filterItems, setFilterItems, filters, @@ -47,12 +47,12 @@ const TableSampleEnrollments = ({ ]); const { - enrollments, + course, loading, count, - notify: enrollmentsNotify, + notify: courseNotify, refetch, - } = useAppSelector((state) => state.enrollments); + } = useAppSelector((state) => state.course); const { currentUser } = useAppSelector((state) => state.auth); const focusRing = useAppSelector((state) => state.style.focusRingColor); const bgColor = useAppSelector((state) => state.style.bgLayoutColor); @@ -73,13 +73,10 @@ const TableSampleEnrollments = ({ }; useEffect(() => { - if (enrollmentsNotify.showNotification) { - notify( - enrollmentsNotify.typeNotification, - enrollmentsNotify.textNotification, - ); + if (courseNotify.showNotification) { + notify(courseNotify.typeNotification, courseNotify.textNotification); } - }, [enrollmentsNotify.showNotification]); + }, [courseNotify.showNotification]); useEffect(() => { if (!currentUser) return; @@ -184,7 +181,7 @@ const TableSampleEnrollments = ({ useEffect(() => { if (!currentUser) return; - loadColumns(handleDeleteModalAction, `enrollments`, currentUser).then( + loadColumns(handleDeleteModalAction, `course`, currentUser).then( (newCols) => setColumns(newCols), ); }, [currentUser]); @@ -218,7 +215,7 @@ const TableSampleEnrollments = ({ sx={dataGridStyles} className={'datagrid--table'} getRowClassName={() => `datagrid--row`} - rows={enrollments ?? []} + rows={course ?? []} columns={columns} initialState={{ pagination: { @@ -484,4 +481,4 @@ const TableSampleEnrollments = ({ ); }; -export default TableSampleEnrollments; +export default TableSampleCourse; diff --git a/frontend/src/components/Course/configureCourseCols.tsx b/frontend/src/components/Course/configureCourseCols.tsx new file mode 100644 index 0000000..89d8e72 --- /dev/null +++ b/frontend/src/components/Course/configureCourseCols.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_COURSE'); + + return [ + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
    + +
    , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Courses/CardCourses.tsx b/frontend/src/components/Courses/CardCourses.tsx deleted file mode 100644 index 8ee7ddb..0000000 --- a/frontend/src/components/Courses/CardCourses.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import React from 'react'; -import ImageField from '../ImageField'; -import ListActionsPopover from '../ListActionsPopover'; -import { useAppSelector } from '../../stores/hooks'; -import dataFormatter from '../../helpers/dataFormatter'; -import { Pagination } from '../Pagination'; -import { saveFile } from '../../helpers/fileSaver'; -import LoadingSpinner from '../LoadingSpinner'; -import Link from 'next/link'; - -import { hasPermission } from '../../helpers/userPermissions'; - -type Props = { - courses: any[]; - loading: boolean; - onDelete: (id: string) => void; - currentPage: number; - numPages: number; - onPageChange: (page: number) => void; -}; - -const CardCourses = ({ - courses, - loading, - onDelete, - currentPage, - numPages, - onPageChange, -}: Props) => { - const asideScrollbarsStyle = useAppSelector( - (state) => state.style.asideScrollbarsStyle, - ); - const bgColor = useAppSelector((state) => state.style.cardsColor); - const darkMode = useAppSelector((state) => state.style.darkMode); - const corners = useAppSelector((state) => state.style.corners); - const focusRing = useAppSelector((state) => state.style.focusRingColor); - - const currentUser = useAppSelector((state) => state.auth.currentUser); - const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_COURSES'); - - return ( -
    - {loading && } -
      - {!loading && - courses.map((item, index) => ( -
    • -
      - - {item.title} - - -
      - -
      -
      -
      -
      -
      Title
      -
      -
      {item.title}
      -
      -
      - -
      -
      - Description -
      -
      -
      - {item.description} -
      -
      -
      - -
      -
      - Instructors -
      -
      -
      - {dataFormatter - .usersManyListFormatter(item.instructors) - .join(', ')} -
      -
      -
      - -
      -
      - Students -
      -
      -
      - {dataFormatter - .usersManyListFormatter(item.students) - .join(', ')} -
      -
      -
      - -
      -
      - DiscussionBoards -
      -
      -
      - {dataFormatter - .discussion_boardsManyListFormatter( - item.discussion_boards, - ) - .join(', ')} -
      -
      -
      -
      -
    • - ))} - {!loading && courses.length === 0 && ( -
      -

      No data to display

      -
      - )} -
    -
    - -
    -
    - ); -}; - -export default CardCourses; diff --git a/frontend/src/components/Courses/ListCourses.tsx b/frontend/src/components/Courses/ListCourses.tsx deleted file mode 100644 index 234e179..0000000 --- a/frontend/src/components/Courses/ListCourses.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import React from 'react'; -import CardBox from '../CardBox'; -import ImageField from '../ImageField'; -import dataFormatter from '../../helpers/dataFormatter'; -import { saveFile } from '../../helpers/fileSaver'; -import ListActionsPopover from '../ListActionsPopover'; -import { useAppSelector } from '../../stores/hooks'; -import { Pagination } from '../Pagination'; -import LoadingSpinner from '../LoadingSpinner'; -import Link from 'next/link'; - -import { hasPermission } from '../../helpers/userPermissions'; - -type Props = { - courses: any[]; - loading: boolean; - onDelete: (id: string) => void; - currentPage: number; - numPages: number; - onPageChange: (page: number) => void; -}; - -const ListCourses = ({ - courses, - loading, - onDelete, - currentPage, - numPages, - onPageChange, -}: Props) => { - const currentUser = useAppSelector((state) => state.auth.currentUser); - const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_COURSES'); - - const corners = useAppSelector((state) => state.style.corners); - const bgColor = useAppSelector((state) => state.style.cardsColor); - - return ( - <> -
    - {loading && } - {!loading && - courses.map((item) => ( -
    - -
    - dark:divide-dark-700 overflow-x-auto' - } - > -
    -

    Title

    -

    {item.title}

    -
    - -
    -

    Description

    -

    {item.description}

    -
    - -
    -

    Instructors

    -

    - {dataFormatter - .usersManyListFormatter(item.instructors) - .join(', ')} -

    -
    - -
    -

    Students

    -

    - {dataFormatter - .usersManyListFormatter(item.students) - .join(', ')} -

    -
    - -
    -

    - DiscussionBoards -

    -

    - {dataFormatter - .discussion_boardsManyListFormatter( - item.discussion_boards, - ) - .join(', ')} -

    -
    - - -
    -
    -
    - ))} - {!loading && courses.length === 0 && ( -
    -

    No data to display

    -
    - )} -
    -
    - -
    - - ); -}; - -export default ListCourses; diff --git a/frontend/src/components/Courses/configureCoursesCols.tsx b/frontend/src/components/Courses/configureCoursesCols.tsx deleted file mode 100644 index 56d03c7..0000000 --- a/frontend/src/components/Courses/configureCoursesCols.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import React from 'react'; -import BaseIcon from '../BaseIcon'; -import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; -import axios from 'axios'; -import { - GridActionsCellItem, - GridRowParams, - GridValueGetterParams, -} from '@mui/x-data-grid'; -import ImageField from '../ImageField'; -import { saveFile } from '../../helpers/fileSaver'; -import dataFormatter from '../../helpers/dataFormatter'; -import DataGridMultiSelect from '../DataGridMultiSelect'; -import ListActionsPopover from '../ListActionsPopover'; - -import { hasPermission } from '../../helpers/userPermissions'; - -type Params = (id: string) => void; - -export const loadColumns = async ( - onDelete: Params, - entityName: string, - - user, -) => { - async function callOptionsApi(entityName: string) { - if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; - - try { - const data = await axios(`/${entityName}/autocomplete?limit=100`); - return data.data; - } catch (error) { - console.log(error); - return []; - } - } - - const hasUpdatePermission = hasPermission(user, 'UPDATE_COURSES'); - - return [ - { - field: 'title', - headerName: 'Title', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - }, - - { - field: 'description', - headerName: 'Description', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - }, - - { - field: 'instructors', - headerName: 'Instructors', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: false, - sortable: false, - type: 'singleSelect', - valueFormatter: ({ value }) => - dataFormatter.usersManyListFormatter(value).join(', '), - renderEditCell: (params) => ( - - ), - }, - - { - field: 'students', - headerName: 'Students', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: false, - sortable: false, - type: 'singleSelect', - valueFormatter: ({ value }) => - dataFormatter.usersManyListFormatter(value).join(', '), - renderEditCell: (params) => ( - - ), - }, - - { - field: 'discussion_boards', - headerName: 'DiscussionBoards', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: false, - sortable: false, - type: 'singleSelect', - valueFormatter: ({ value }) => - dataFormatter.discussion_boardsManyListFormatter(value).join(', '), - renderEditCell: (params) => ( - - ), - }, - - { - field: 'actions', - type: 'actions', - minWidth: 30, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - getActions: (params: GridRowParams) => { - return [ -
    - -
    , - ]; - }, - }, - ]; -}; diff --git a/frontend/src/components/Discussion_boards/CardDiscussion_boards.tsx b/frontend/src/components/Discussion_boards/CardDiscussion_boards.tsx deleted file mode 100644 index 67413b3..0000000 --- a/frontend/src/components/Discussion_boards/CardDiscussion_boards.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import React from 'react'; -import ImageField from '../ImageField'; -import ListActionsPopover from '../ListActionsPopover'; -import { useAppSelector } from '../../stores/hooks'; -import dataFormatter from '../../helpers/dataFormatter'; -import { Pagination } from '../Pagination'; -import { saveFile } from '../../helpers/fileSaver'; -import LoadingSpinner from '../LoadingSpinner'; -import Link from 'next/link'; - -import { hasPermission } from '../../helpers/userPermissions'; - -type Props = { - discussion_boards: any[]; - loading: boolean; - onDelete: (id: string) => void; - currentPage: number; - numPages: number; - onPageChange: (page: number) => void; -}; - -const CardDiscussion_boards = ({ - discussion_boards, - loading, - onDelete, - currentPage, - numPages, - onPageChange, -}: Props) => { - const asideScrollbarsStyle = useAppSelector( - (state) => state.style.asideScrollbarsStyle, - ); - const bgColor = useAppSelector((state) => state.style.cardsColor); - const darkMode = useAppSelector((state) => state.style.darkMode); - const corners = useAppSelector((state) => state.style.corners); - const focusRing = useAppSelector((state) => state.style.focusRingColor); - - const currentUser = useAppSelector((state) => state.auth.currentUser); - const hasUpdatePermission = hasPermission( - currentUser, - 'UPDATE_DISCUSSION_BOARDS', - ); - - return ( -
    - {loading && } -
      - {!loading && - discussion_boards.map((item, index) => ( -
    • -
      - - {item.topic} - - -
      - -
      -
      -
      -
      -
      - Course -
      -
      -
      - {dataFormatter.coursesOneListFormatter(item.course)} -
      -
      -
      - -
      -
      Topic
      -
      -
      {item.topic}
      -
      -
      - -
      -
      Posts
      -
      -
      - {dataFormatter - .postsManyListFormatter(item.posts) - .join(', ')} -
      -
      -
      -
      -
    • - ))} - {!loading && discussion_boards.length === 0 && ( -
      -

      No data to display

      -
      - )} -
    -
    - -
    -
    - ); -}; - -export default CardDiscussion_boards; diff --git a/frontend/src/components/Discussion_boards/ListDiscussion_boards.tsx b/frontend/src/components/Discussion_boards/ListDiscussion_boards.tsx deleted file mode 100644 index 32f3f57..0000000 --- a/frontend/src/components/Discussion_boards/ListDiscussion_boards.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import React from 'react'; -import CardBox from '../CardBox'; -import ImageField from '../ImageField'; -import dataFormatter from '../../helpers/dataFormatter'; -import { saveFile } from '../../helpers/fileSaver'; -import ListActionsPopover from '../ListActionsPopover'; -import { useAppSelector } from '../../stores/hooks'; -import { Pagination } from '../Pagination'; -import LoadingSpinner from '../LoadingSpinner'; -import Link from 'next/link'; - -import { hasPermission } from '../../helpers/userPermissions'; - -type Props = { - discussion_boards: any[]; - loading: boolean; - onDelete: (id: string) => void; - currentPage: number; - numPages: number; - onPageChange: (page: number) => void; -}; - -const ListDiscussion_boards = ({ - discussion_boards, - loading, - onDelete, - currentPage, - numPages, - onPageChange, -}: Props) => { - const currentUser = useAppSelector((state) => state.auth.currentUser); - const hasUpdatePermission = hasPermission( - currentUser, - 'UPDATE_DISCUSSION_BOARDS', - ); - - const corners = useAppSelector((state) => state.style.corners); - const bgColor = useAppSelector((state) => state.style.cardsColor); - - return ( - <> -
    - {loading && } - {!loading && - discussion_boards.map((item) => ( -
    - -
    - dark:divide-dark-700 overflow-x-auto' - } - > -
    -

    Course

    -

    - {dataFormatter.coursesOneListFormatter(item.course)} -

    -
    - -
    -

    Topic

    -

    {item.topic}

    -
    - -
    -

    Posts

    -

    - {dataFormatter - .postsManyListFormatter(item.posts) - .join(', ')} -

    -
    - - -
    -
    -
    - ))} - {!loading && discussion_boards.length === 0 && ( -
    -

    No data to display

    -
    - )} -
    -
    - -
    - - ); -}; - -export default ListDiscussion_boards; diff --git a/frontend/src/components/Discussion_boards/TableDiscussion_boards.tsx b/frontend/src/components/Discussion_boards/TableDiscussion_boards.tsx deleted file mode 100644 index 10adab1..0000000 --- a/frontend/src/components/Discussion_boards/TableDiscussion_boards.tsx +++ /dev/null @@ -1,500 +0,0 @@ -import React, { useEffect, useState, useMemo } from 'react'; -import { createPortal } from 'react-dom'; -import { ToastContainer, toast } from 'react-toastify'; -import BaseButton from '../BaseButton'; -import CardBoxModal from '../CardBoxModal'; -import CardBox from '../CardBox'; -import { - fetch, - update, - deleteItem, - setRefetch, - deleteItemsByIds, -} from '../../stores/discussion_boards/discussion_boardsSlice'; -import { useAppDispatch, useAppSelector } from '../../stores/hooks'; -import { useRouter } from 'next/router'; -import { Field, Form, Formik } from 'formik'; -import { DataGrid, GridColDef } from '@mui/x-data-grid'; -import { loadColumns } from './configureDiscussion_boardsCols'; -import _ from 'lodash'; -import dataFormatter from '../../helpers/dataFormatter'; -import { dataGridStyles } from '../../styles'; - -import ListDiscussion_boards from './ListDiscussion_boards'; - -const perPage = 10; - -const TableSampleDiscussion_boards = ({ - filterItems, - setFilterItems, - filters, - showGrid, -}) => { - const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); - - const dispatch = useAppDispatch(); - const router = useRouter(); - - const pagesList = []; - const [id, setId] = useState(null); - const [currentPage, setCurrentPage] = useState(0); - const [filterRequest, setFilterRequest] = React.useState(''); - const [columns, setColumns] = useState([]); - const [selectedRows, setSelectedRows] = useState([]); - const [sortModel, setSortModel] = useState([ - { - field: '', - sort: 'desc', - }, - ]); - - const { - discussion_boards, - loading, - count, - notify: discussion_boardsNotify, - refetch, - } = useAppSelector((state) => state.discussion_boards); - const { currentUser } = useAppSelector((state) => state.auth); - const focusRing = useAppSelector((state) => state.style.focusRingColor); - const bgColor = useAppSelector((state) => state.style.bgLayoutColor); - const corners = useAppSelector((state) => state.style.corners); - const numPages = - Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); - for (let i = 0; i < numPages; i++) { - pagesList.push(i); - } - - const loadData = async (page = currentPage, request = filterRequest) => { - if (page !== currentPage) setCurrentPage(page); - if (request !== filterRequest) setFilterRequest(request); - const { sort, field } = sortModel[0]; - - const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; - dispatch(fetch({ limit: perPage, page, query })); - }; - - useEffect(() => { - if (discussion_boardsNotify.showNotification) { - notify( - discussion_boardsNotify.typeNotification, - discussion_boardsNotify.textNotification, - ); - } - }, [discussion_boardsNotify.showNotification]); - - useEffect(() => { - if (!currentUser) return; - loadData(); - }, [sortModel, currentUser]); - - useEffect(() => { - if (refetch) { - loadData(0); - dispatch(setRefetch(false)); - } - }, [refetch, dispatch]); - - const [isModalInfoActive, setIsModalInfoActive] = useState(false); - const [isModalTrashActive, setIsModalTrashActive] = useState(false); - - const handleModalAction = () => { - setIsModalInfoActive(false); - setIsModalTrashActive(false); - }; - - const handleDeleteModalAction = (id: string) => { - setId(id); - setIsModalTrashActive(true); - }; - const handleDeleteAction = async () => { - if (id) { - await dispatch(deleteItem(id)); - await loadData(0); - setIsModalTrashActive(false); - } - }; - - const generateFilterRequests = useMemo(() => { - let request = '&'; - filterItems.forEach((item) => { - const isRangeFilter = filters.find( - (filter) => - filter.title === item.fields.selectedField && - (filter.number || filter.date), - ); - - if (isRangeFilter) { - const from = item.fields.filterValueFrom; - const to = item.fields.filterValueTo; - if (from) { - request += `${item.fields.selectedField}Range=${from}&`; - } - if (to) { - request += `${item.fields.selectedField}Range=${to}&`; - } - } else { - const value = item.fields.filterValue; - if (value) { - request += `${item.fields.selectedField}=${value}&`; - } - } - }); - return request; - }, [filterItems, filters]); - - const deleteFilter = (value) => { - const newItems = filterItems.filter((item) => item.id !== value); - - if (newItems.length) { - setFilterItems(newItems); - } else { - loadData(0, ''); - - setFilterItems(newItems); - } - }; - - const handleSubmit = () => { - loadData(0, generateFilterRequests); - }; - - const handleChange = (id) => (e) => { - const value = e.target.value; - const name = e.target.name; - - setFilterItems( - filterItems.map((item) => { - if (item.id !== id) return item; - if (name === 'selectedField') return { id, fields: { [name]: value } }; - - return { id, fields: { ...item.fields, [name]: value } }; - }), - ); - }; - - const handleReset = () => { - setFilterItems([]); - loadData(0, ''); - }; - - const onPageChange = (page: number) => { - loadData(page); - setCurrentPage(page); - }; - - useEffect(() => { - if (!currentUser) return; - - loadColumns(handleDeleteModalAction, `discussion_boards`, currentUser).then( - (newCols) => setColumns(newCols), - ); - }, [currentUser]); - - const handleTableSubmit = async (id: string, data) => { - if (!_.isEmpty(data)) { - await dispatch(update({ id, data })) - .unwrap() - .then((res) => res) - .catch((err) => { - throw new Error(err); - }); - } - }; - - const onDeleteRows = async (selectedRows) => { - await dispatch(deleteItemsByIds(selectedRows)); - await loadData(0); - }; - - const controlClasses = - 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + - ` ${bgColor} ${focusRing} ${corners} ` + - 'dark:bg-slate-800 border'; - - const dataGrid = ( -
    - `datagrid--row`} - rows={discussion_boards ?? []} - columns={columns} - initialState={{ - pagination: { - paginationModel: { - pageSize: 10, - }, - }, - }} - disableRowSelectionOnClick - onProcessRowUpdateError={(params) => { - console.log('Error', params); - }} - processRowUpdate={async (newRow, oldRow) => { - const data = dataFormatter.dataGridEditFormatter(newRow); - - try { - await handleTableSubmit(newRow.id, data); - return newRow; - } catch { - return oldRow; - } - }} - sortingMode={'server'} - checkboxSelection - onRowSelectionModelChange={(ids) => { - setSelectedRows(ids); - }} - onSortModelChange={(params) => { - params.length - ? setSortModel(params) - : setSortModel([{ field: '', sort: 'desc' }]); - }} - rowCount={count} - pageSizeOptions={[10]} - paginationMode={'server'} - loading={loading} - onPaginationModelChange={(params) => { - onPageChange(params.page); - }} - /> -
    - ); - - return ( - <> - {filterItems && Array.isArray(filterItems) && filterItems.length ? ( - - null} - > -
    - <> - {filterItems && - filterItems.map((filterItem) => { - return ( -
    -
    -
    - Filter -
    - - {filters.map((selectOption) => ( - - ))} - -
    - {filters.find( - (filter) => - filter.title === filterItem?.fields?.selectedField, - )?.type === 'enum' ? ( -
    -
    Value
    - - - {filters - .find( - (filter) => - filter.title === - filterItem?.fields?.selectedField, - ) - ?.options?.map((option) => ( - - ))} - -
    - ) : filters.find( - (filter) => - filter.title === - filterItem?.fields?.selectedField, - )?.number ? ( -
    -
    -
    - From -
    - -
    -
    -
    - To -
    - -
    -
    - ) : filters.find( - (filter) => - filter.title === - filterItem?.fields?.selectedField, - )?.date ? ( -
    -
    -
    - From -
    - -
    -
    -
    - To -
    - -
    -
    - ) : ( -
    -
    - Contains -
    - -
    - )} -
    -
    - Action -
    - { - deleteFilter(filterItem.id); - }} - /> -
    -
    - ); - })} -
    - - -
    - -
    -
    -
    - ) : null} - -

    Are you sure you want to delete this item?

    -
    - - {discussion_boards && Array.isArray(discussion_boards) && !showGrid && ( - - )} - - {showGrid && dataGrid} - - {selectedRows.length > 0 && - createPortal( - onDeleteRows(selectedRows)} - />, - document.getElementById('delete-rows-button'), - )} - - - ); -}; - -export default TableSampleDiscussion_boards; diff --git a/frontend/src/components/Discussion_boards/configureDiscussion_boardsCols.tsx b/frontend/src/components/Discussion_boards/configureDiscussion_boardsCols.tsx deleted file mode 100644 index b7b6dfc..0000000 --- a/frontend/src/components/Discussion_boards/configureDiscussion_boardsCols.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React from 'react'; -import BaseIcon from '../BaseIcon'; -import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; -import axios from 'axios'; -import { - GridActionsCellItem, - GridRowParams, - GridValueGetterParams, -} from '@mui/x-data-grid'; -import ImageField from '../ImageField'; -import { saveFile } from '../../helpers/fileSaver'; -import dataFormatter from '../../helpers/dataFormatter'; -import DataGridMultiSelect from '../DataGridMultiSelect'; -import ListActionsPopover from '../ListActionsPopover'; - -import { hasPermission } from '../../helpers/userPermissions'; - -type Params = (id: string) => void; - -export const loadColumns = async ( - onDelete: Params, - entityName: string, - - user, -) => { - async function callOptionsApi(entityName: string) { - if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; - - try { - const data = await axios(`/${entityName}/autocomplete?limit=100`); - return data.data; - } catch (error) { - console.log(error); - return []; - } - } - - const hasUpdatePermission = hasPermission(user, 'UPDATE_DISCUSSION_BOARDS'); - - return [ - { - field: 'course', - headerName: 'Course', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - sortable: false, - type: 'singleSelect', - getOptionValue: (value: any) => value?.id, - getOptionLabel: (value: any) => value?.label, - valueOptions: await callOptionsApi('courses'), - valueGetter: (params: GridValueGetterParams) => - params?.value?.id ?? params?.value, - }, - - { - field: 'topic', - headerName: 'Topic', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - }, - - { - field: 'posts', - headerName: 'Posts', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: false, - sortable: false, - type: 'singleSelect', - valueFormatter: ({ value }) => - dataFormatter.postsManyListFormatter(value).join(', '), - renderEditCell: (params) => ( - - ), - }, - - { - field: 'actions', - type: 'actions', - minWidth: 30, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - getActions: (params: GridRowParams) => { - return [ -
    - -
    , - ]; - }, - }, - ]; -}; diff --git a/frontend/src/components/Enrollments/configureEnrollmentsCols.tsx b/frontend/src/components/Enrollments/configureEnrollmentsCols.tsx deleted file mode 100644 index b1956c4..0000000 --- a/frontend/src/components/Enrollments/configureEnrollmentsCols.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import React from 'react'; -import BaseIcon from '../BaseIcon'; -import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; -import axios from 'axios'; -import { - GridActionsCellItem, - GridRowParams, - GridValueGetterParams, -} from '@mui/x-data-grid'; -import ImageField from '../ImageField'; -import { saveFile } from '../../helpers/fileSaver'; -import dataFormatter from '../../helpers/dataFormatter'; -import DataGridMultiSelect from '../DataGridMultiSelect'; -import ListActionsPopover from '../ListActionsPopover'; - -import { hasPermission } from '../../helpers/userPermissions'; - -type Params = (id: string) => void; - -export const loadColumns = async ( - onDelete: Params, - entityName: string, - - user, -) => { - async function callOptionsApi(entityName: string) { - if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; - - try { - const data = await axios(`/${entityName}/autocomplete?limit=100`); - return data.data; - } catch (error) { - console.log(error); - return []; - } - } - - const hasUpdatePermission = hasPermission(user, 'UPDATE_ENROLLMENTS'); - - return [ - { - field: 'student', - headerName: 'Student', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - sortable: false, - type: 'singleSelect', - getOptionValue: (value: any) => value?.id, - getOptionLabel: (value: any) => value?.label, - valueOptions: await callOptionsApi('students'), - valueGetter: (params: GridValueGetterParams) => - params?.value?.id ?? params?.value, - }, - - { - field: 'course', - headerName: 'Course', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - sortable: false, - type: 'singleSelect', - getOptionValue: (value: any) => value?.id, - getOptionLabel: (value: any) => value?.label, - valueOptions: await callOptionsApi('courses'), - valueGetter: (params: GridValueGetterParams) => - params?.value?.id ?? params?.value, - }, - - { - field: 'payment_status', - headerName: 'PaymentStatus', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - }, - - { - field: 'actions', - type: 'actions', - minWidth: 30, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - getActions: (params: GridRowParams) => { - return [ -
    - -
    , - ]; - }, - }, - ]; -}; diff --git a/frontend/src/components/Grades/configureGradesCols.tsx b/frontend/src/components/Grades/configureGradesCols.tsx deleted file mode 100644 index a8196fd..0000000 --- a/frontend/src/components/Grades/configureGradesCols.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import React from 'react'; -import BaseIcon from '../BaseIcon'; -import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; -import axios from 'axios'; -import { - GridActionsCellItem, - GridRowParams, - GridValueGetterParams, -} from '@mui/x-data-grid'; -import ImageField from '../ImageField'; -import { saveFile } from '../../helpers/fileSaver'; -import dataFormatter from '../../helpers/dataFormatter'; -import DataGridMultiSelect from '../DataGridMultiSelect'; -import ListActionsPopover from '../ListActionsPopover'; - -import { hasPermission } from '../../helpers/userPermissions'; - -type Params = (id: string) => void; - -export const loadColumns = async ( - onDelete: Params, - entityName: string, - - user, -) => { - async function callOptionsApi(entityName: string) { - if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; - - try { - const data = await axios(`/${entityName}/autocomplete?limit=100`); - return data.data; - } catch (error) { - console.log(error); - return []; - } - } - - const hasUpdatePermission = hasPermission(user, 'UPDATE_GRADES'); - - return [ - { - field: 'student', - headerName: 'Student', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - sortable: false, - type: 'singleSelect', - getOptionValue: (value: any) => value?.id, - getOptionLabel: (value: any) => value?.label, - valueOptions: await callOptionsApi('students'), - valueGetter: (params: GridValueGetterParams) => - params?.value?.id ?? params?.value, - }, - - { - field: 'course', - headerName: 'Course', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - sortable: false, - type: 'singleSelect', - getOptionValue: (value: any) => value?.id, - getOptionLabel: (value: any) => value?.label, - valueOptions: await callOptionsApi('courses'), - valueGetter: (params: GridValueGetterParams) => - params?.value?.id ?? params?.value, - }, - - { - field: 'grade', - headerName: 'Grade', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - type: 'number', - }, - - { - field: 'actions', - type: 'actions', - minWidth: 30, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - getActions: (params: GridRowParams) => { - return [ -
    - -
    , - ]; - }, - }, - ]; -}; diff --git a/frontend/src/components/Instructors/TableInstructors.tsx b/frontend/src/components/Instructors/TableInstructors.tsx deleted file mode 100644 index 6fc38c0..0000000 --- a/frontend/src/components/Instructors/TableInstructors.tsx +++ /dev/null @@ -1,500 +0,0 @@ -import React, { useEffect, useState, useMemo } from 'react'; -import { createPortal } from 'react-dom'; -import { ToastContainer, toast } from 'react-toastify'; -import BaseButton from '../BaseButton'; -import CardBoxModal from '../CardBoxModal'; -import CardBox from '../CardBox'; -import { - fetch, - update, - deleteItem, - setRefetch, - deleteItemsByIds, -} from '../../stores/instructors/instructorsSlice'; -import { useAppDispatch, useAppSelector } from '../../stores/hooks'; -import { useRouter } from 'next/router'; -import { Field, Form, Formik } from 'formik'; -import { DataGrid, GridColDef } from '@mui/x-data-grid'; -import { loadColumns } from './configureInstructorsCols'; -import _ from 'lodash'; -import dataFormatter from '../../helpers/dataFormatter'; -import { dataGridStyles } from '../../styles'; - -import ListInstructors from './ListInstructors'; - -const perPage = 10; - -const TableSampleInstructors = ({ - filterItems, - setFilterItems, - filters, - showGrid, -}) => { - const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); - - const dispatch = useAppDispatch(); - const router = useRouter(); - - const pagesList = []; - const [id, setId] = useState(null); - const [currentPage, setCurrentPage] = useState(0); - const [filterRequest, setFilterRequest] = React.useState(''); - const [columns, setColumns] = useState([]); - const [selectedRows, setSelectedRows] = useState([]); - const [sortModel, setSortModel] = useState([ - { - field: '', - sort: 'desc', - }, - ]); - - const { - instructors, - loading, - count, - notify: instructorsNotify, - refetch, - } = useAppSelector((state) => state.instructors); - const { currentUser } = useAppSelector((state) => state.auth); - const focusRing = useAppSelector((state) => state.style.focusRingColor); - const bgColor = useAppSelector((state) => state.style.bgLayoutColor); - const corners = useAppSelector((state) => state.style.corners); - const numPages = - Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); - for (let i = 0; i < numPages; i++) { - pagesList.push(i); - } - - const loadData = async (page = currentPage, request = filterRequest) => { - if (page !== currentPage) setCurrentPage(page); - if (request !== filterRequest) setFilterRequest(request); - const { sort, field } = sortModel[0]; - - const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; - dispatch(fetch({ limit: perPage, page, query })); - }; - - useEffect(() => { - if (instructorsNotify.showNotification) { - notify( - instructorsNotify.typeNotification, - instructorsNotify.textNotification, - ); - } - }, [instructorsNotify.showNotification]); - - useEffect(() => { - if (!currentUser) return; - loadData(); - }, [sortModel, currentUser]); - - useEffect(() => { - if (refetch) { - loadData(0); - dispatch(setRefetch(false)); - } - }, [refetch, dispatch]); - - const [isModalInfoActive, setIsModalInfoActive] = useState(false); - const [isModalTrashActive, setIsModalTrashActive] = useState(false); - - const handleModalAction = () => { - setIsModalInfoActive(false); - setIsModalTrashActive(false); - }; - - const handleDeleteModalAction = (id: string) => { - setId(id); - setIsModalTrashActive(true); - }; - const handleDeleteAction = async () => { - if (id) { - await dispatch(deleteItem(id)); - await loadData(0); - setIsModalTrashActive(false); - } - }; - - const generateFilterRequests = useMemo(() => { - let request = '&'; - filterItems.forEach((item) => { - const isRangeFilter = filters.find( - (filter) => - filter.title === item.fields.selectedField && - (filter.number || filter.date), - ); - - if (isRangeFilter) { - const from = item.fields.filterValueFrom; - const to = item.fields.filterValueTo; - if (from) { - request += `${item.fields.selectedField}Range=${from}&`; - } - if (to) { - request += `${item.fields.selectedField}Range=${to}&`; - } - } else { - const value = item.fields.filterValue; - if (value) { - request += `${item.fields.selectedField}=${value}&`; - } - } - }); - return request; - }, [filterItems, filters]); - - const deleteFilter = (value) => { - const newItems = filterItems.filter((item) => item.id !== value); - - if (newItems.length) { - setFilterItems(newItems); - } else { - loadData(0, ''); - - setFilterItems(newItems); - } - }; - - const handleSubmit = () => { - loadData(0, generateFilterRequests); - }; - - const handleChange = (id) => (e) => { - const value = e.target.value; - const name = e.target.name; - - setFilterItems( - filterItems.map((item) => { - if (item.id !== id) return item; - if (name === 'selectedField') return { id, fields: { [name]: value } }; - - return { id, fields: { ...item.fields, [name]: value } }; - }), - ); - }; - - const handleReset = () => { - setFilterItems([]); - loadData(0, ''); - }; - - const onPageChange = (page: number) => { - loadData(page); - setCurrentPage(page); - }; - - useEffect(() => { - if (!currentUser) return; - - loadColumns(handleDeleteModalAction, `instructors`, currentUser).then( - (newCols) => setColumns(newCols), - ); - }, [currentUser]); - - const handleTableSubmit = async (id: string, data) => { - if (!_.isEmpty(data)) { - await dispatch(update({ id, data })) - .unwrap() - .then((res) => res) - .catch((err) => { - throw new Error(err); - }); - } - }; - - const onDeleteRows = async (selectedRows) => { - await dispatch(deleteItemsByIds(selectedRows)); - await loadData(0); - }; - - const controlClasses = - 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + - ` ${bgColor} ${focusRing} ${corners} ` + - 'dark:bg-slate-800 border'; - - const dataGrid = ( -
    - `datagrid--row`} - rows={instructors ?? []} - columns={columns} - initialState={{ - pagination: { - paginationModel: { - pageSize: 10, - }, - }, - }} - disableRowSelectionOnClick - onProcessRowUpdateError={(params) => { - console.log('Error', params); - }} - processRowUpdate={async (newRow, oldRow) => { - const data = dataFormatter.dataGridEditFormatter(newRow); - - try { - await handleTableSubmit(newRow.id, data); - return newRow; - } catch { - return oldRow; - } - }} - sortingMode={'server'} - checkboxSelection - onRowSelectionModelChange={(ids) => { - setSelectedRows(ids); - }} - onSortModelChange={(params) => { - params.length - ? setSortModel(params) - : setSortModel([{ field: '', sort: 'desc' }]); - }} - rowCount={count} - pageSizeOptions={[10]} - paginationMode={'server'} - loading={loading} - onPaginationModelChange={(params) => { - onPageChange(params.page); - }} - /> -
    - ); - - return ( - <> - {filterItems && Array.isArray(filterItems) && filterItems.length ? ( - - null} - > -
    - <> - {filterItems && - filterItems.map((filterItem) => { - return ( -
    -
    -
    - Filter -
    - - {filters.map((selectOption) => ( - - ))} - -
    - {filters.find( - (filter) => - filter.title === filterItem?.fields?.selectedField, - )?.type === 'enum' ? ( -
    -
    Value
    - - - {filters - .find( - (filter) => - filter.title === - filterItem?.fields?.selectedField, - ) - ?.options?.map((option) => ( - - ))} - -
    - ) : filters.find( - (filter) => - filter.title === - filterItem?.fields?.selectedField, - )?.number ? ( -
    -
    -
    - From -
    - -
    -
    -
    - To -
    - -
    -
    - ) : filters.find( - (filter) => - filter.title === - filterItem?.fields?.selectedField, - )?.date ? ( -
    -
    -
    - From -
    - -
    -
    -
    - To -
    - -
    -
    - ) : ( -
    -
    - Contains -
    - -
    - )} -
    -
    - Action -
    - { - deleteFilter(filterItem.id); - }} - /> -
    -
    - ); - })} -
    - - -
    - -
    -
    -
    - ) : null} - -

    Are you sure you want to delete this item?

    -
    - - {instructors && Array.isArray(instructors) && !showGrid && ( - - )} - - {showGrid && dataGrid} - - {selectedRows.length > 0 && - createPortal( - onDeleteRows(selectedRows)} - />, - document.getElementById('delete-rows-button'), - )} - - - ); -}; - -export default TableSampleInstructors; diff --git a/frontend/src/components/Instructors/configureInstructorsCols.tsx b/frontend/src/components/Instructors/configureInstructorsCols.tsx deleted file mode 100644 index d1085e6..0000000 --- a/frontend/src/components/Instructors/configureInstructorsCols.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React from 'react'; -import BaseIcon from '../BaseIcon'; -import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; -import axios from 'axios'; -import { - GridActionsCellItem, - GridRowParams, - GridValueGetterParams, -} from '@mui/x-data-grid'; -import ImageField from '../ImageField'; -import { saveFile } from '../../helpers/fileSaver'; -import dataFormatter from '../../helpers/dataFormatter'; -import DataGridMultiSelect from '../DataGridMultiSelect'; -import ListActionsPopover from '../ListActionsPopover'; - -import { hasPermission } from '../../helpers/userPermissions'; - -type Params = (id: string) => void; - -export const loadColumns = async ( - onDelete: Params, - entityName: string, - - user, -) => { - async function callOptionsApi(entityName: string) { - if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; - - try { - const data = await axios(`/${entityName}/autocomplete?limit=100`); - return data.data; - } catch (error) { - console.log(error); - return []; - } - } - - const hasUpdatePermission = hasPermission(user, 'UPDATE_INSTRUCTORS'); - - return [ - { - field: 'user', - headerName: 'User', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - sortable: false, - type: 'singleSelect', - getOptionValue: (value: any) => value?.id, - getOptionLabel: (value: any) => value?.label, - valueOptions: await callOptionsApi('users'), - valueGetter: (params: GridValueGetterParams) => - params?.value?.id ?? params?.value, - }, - - { - field: 'qualifications', - headerName: 'Qualifications', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - }, - - { - field: 'courses', - headerName: 'Courses', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: false, - sortable: false, - type: 'singleSelect', - valueFormatter: ({ value }) => - dataFormatter.coursesManyListFormatter(value).join(', '), - renderEditCell: (params) => ( - - ), - }, - - { - field: 'actions', - type: 'actions', - minWidth: 30, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - getActions: (params: GridRowParams) => { - return [ -
    - -
    , - ]; - }, - }, - ]; -}; diff --git a/frontend/src/components/Students/CardStudents.tsx b/frontend/src/components/Mcs_pyq/CardMcs_pyq.tsx similarity index 60% rename from frontend/src/components/Students/CardStudents.tsx rename to frontend/src/components/Mcs_pyq/CardMcs_pyq.tsx index 4ac7363..a82133a 100644 --- a/frontend/src/components/Students/CardStudents.tsx +++ b/frontend/src/components/Mcs_pyq/CardMcs_pyq.tsx @@ -11,7 +11,7 @@ import Link from 'next/link'; import { hasPermission } from '../../helpers/userPermissions'; type Props = { - students: any[]; + mcs_pyq: any[]; loading: boolean; onDelete: (id: string) => void; currentPage: number; @@ -19,8 +19,8 @@ type Props = { onPageChange: (page: number) => void; }; -const CardStudents = ({ - students, +const CardMcs_pyq = ({ + mcs_pyq, loading, onDelete, currentPage, @@ -36,7 +36,7 @@ const CardStudents = ({ const focusRing = useAppSelector((state) => state.style.focusRingColor); const currentUser = useAppSelector((state) => state.auth.currentUser); - const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_STUDENTS'); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_MCS_PYQ'); return (
    @@ -46,7 +46,7 @@ const CardStudents = ({ className='grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-3 2xl:grid-cols-4 xl:gap-x-8' > {!loading && - students.map((item, index) => ( + mcs_pyq.map((item, index) => (
  • - {item.user} + {item.id}
  • -
    -
    -
    User
    -
    -
    - {dataFormatter.usersOneListFormatter(item.user)} -
    -
    -
    - -
    -
    - Enrollments -
    -
    -
    - {dataFormatter - .enrollmentsManyListFormatter(item.enrollments) - .join(', ')} -
    -
    -
    - -
    -
    - Grades -
    -
    -
    - {dataFormatter - .gradesManyListFormatter(item.grades) - .join(', ')} -
    -
    -
    -
    +
    ))} - {!loading && students.length === 0 && ( + {!loading && mcs_pyq.length === 0 && (

    No data to display

    @@ -130,4 +95,4 @@ const CardStudents = ({ ); }; -export default CardStudents; +export default CardMcs_pyq; diff --git a/frontend/src/components/Students/ListStudents.tsx b/frontend/src/components/Mcs_pyq/ListMcs_pyq.tsx similarity index 61% rename from frontend/src/components/Students/ListStudents.tsx rename to frontend/src/components/Mcs_pyq/ListMcs_pyq.tsx index ff9f4db..896a561 100644 --- a/frontend/src/components/Students/ListStudents.tsx +++ b/frontend/src/components/Mcs_pyq/ListMcs_pyq.tsx @@ -12,7 +12,7 @@ import Link from 'next/link'; import { hasPermission } from '../../helpers/userPermissions'; type Props = { - students: any[]; + mcs_pyq: any[]; loading: boolean; onDelete: (id: string) => void; currentPage: number; @@ -20,8 +20,8 @@ type Props = { onPageChange: (page: number) => void; }; -const ListStudents = ({ - students, +const ListMcs_pyq = ({ + mcs_pyq, loading, onDelete, currentPage, @@ -29,7 +29,7 @@ const ListStudents = ({ onPageChange, }: Props) => { const currentUser = useAppSelector((state) => state.auth.currentUser); - const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_STUDENTS'); + const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_MCS_PYQ'); const corners = useAppSelector((state) => state.style.corners); const bgColor = useAppSelector((state) => state.style.cardsColor); @@ -39,7 +39,7 @@ const ListStudents = ({
    {loading && } {!loading && - students.map((item) => ( + mcs_pyq.map((item) => (
    dark:divide-dark-700 overflow-x-auto' } - > -
    -

    User

    -

    - {dataFormatter.usersOneListFormatter(item.user)} -

    -
    - -
    -

    Enrollments

    -

    - {dataFormatter - .enrollmentsManyListFormatter(item.enrollments) - .join(', ')} -

    -
    - -
    -

    Grades

    -

    - {dataFormatter - .gradesManyListFormatter(item.grades) - .join(', ')} -

    -
    - + >
    ))} - {!loading && students.length === 0 && ( + {!loading && mcs_pyq.length === 0 && (

    No data to display

    @@ -106,4 +81,4 @@ const ListStudents = ({ ); }; -export default ListStudents; +export default ListMcs_pyq; diff --git a/frontend/src/components/Courses/TableCourses.tsx b/frontend/src/components/Mcs_pyq/TableMcs_pyq.tsx similarity index 94% rename from frontend/src/components/Courses/TableCourses.tsx rename to frontend/src/components/Mcs_pyq/TableMcs_pyq.tsx index f5e9d4a..e9b8058 100644 --- a/frontend/src/components/Courses/TableCourses.tsx +++ b/frontend/src/components/Mcs_pyq/TableMcs_pyq.tsx @@ -10,21 +10,19 @@ import { deleteItem, setRefetch, deleteItemsByIds, -} from '../../stores/courses/coursesSlice'; +} from '../../stores/mcs_pyq/mcs_pyqSlice'; import { useAppDispatch, useAppSelector } from '../../stores/hooks'; import { useRouter } from 'next/router'; import { Field, Form, Formik } from 'formik'; import { DataGrid, GridColDef } from '@mui/x-data-grid'; -import { loadColumns } from './configureCoursesCols'; +import { loadColumns } from './configureMcs_pyqCols'; import _ from 'lodash'; import dataFormatter from '../../helpers/dataFormatter'; import { dataGridStyles } from '../../styles'; -import ListCourses from './ListCourses'; - const perPage = 10; -const TableSampleCourses = ({ +const TableSampleMcs_pyq = ({ filterItems, setFilterItems, filters, @@ -49,12 +47,12 @@ const TableSampleCourses = ({ ]); const { - courses, + mcs_pyq, loading, count, - notify: coursesNotify, + notify: mcs_pyqNotify, refetch, - } = useAppSelector((state) => state.courses); + } = useAppSelector((state) => state.mcs_pyq); const { currentUser } = useAppSelector((state) => state.auth); const focusRing = useAppSelector((state) => state.style.focusRingColor); const bgColor = useAppSelector((state) => state.style.bgLayoutColor); @@ -75,10 +73,10 @@ const TableSampleCourses = ({ }; useEffect(() => { - if (coursesNotify.showNotification) { - notify(coursesNotify.typeNotification, coursesNotify.textNotification); + if (mcs_pyqNotify.showNotification) { + notify(mcs_pyqNotify.typeNotification, mcs_pyqNotify.textNotification); } - }, [coursesNotify.showNotification]); + }, [mcs_pyqNotify.showNotification]); useEffect(() => { if (!currentUser) return; @@ -183,7 +181,7 @@ const TableSampleCourses = ({ useEffect(() => { if (!currentUser) return; - loadColumns(handleDeleteModalAction, `courses`, currentUser).then( + loadColumns(handleDeleteModalAction, `mcs_pyq`, currentUser).then( (newCols) => setColumns(newCols), ); }, [currentUser]); @@ -217,7 +215,7 @@ const TableSampleCourses = ({ sx={dataGridStyles} className={'datagrid--table'} getRowClassName={() => `datagrid--row`} - rows={courses ?? []} + rows={mcs_pyq ?? []} columns={columns} initialState={{ pagination: { @@ -466,18 +464,7 @@ const TableSampleCourses = ({

    Are you sure you want to delete this item?

    - {courses && Array.isArray(courses) && !showGrid && ( - - )} - - {showGrid && dataGrid} + {dataGrid} {selectedRows.length > 0 && createPortal( @@ -494,4 +481,4 @@ const TableSampleCourses = ({ ); }; -export default TableSampleCourses; +export default TableSampleMcs_pyq; diff --git a/frontend/src/components/Mcs_pyq/configureMcs_pyqCols.tsx b/frontend/src/components/Mcs_pyq/configureMcs_pyqCols.tsx new file mode 100644 index 0000000..f699953 --- /dev/null +++ b/frontend/src/components/Mcs_pyq/configureMcs_pyqCols.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import BaseIcon from '../BaseIcon'; +import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; +import axios from 'axios'; +import { + GridActionsCellItem, + GridRowParams, + GridValueGetterParams, +} from '@mui/x-data-grid'; +import ImageField from '../ImageField'; +import { saveFile } from '../../helpers/fileSaver'; +import dataFormatter from '../../helpers/dataFormatter'; +import DataGridMultiSelect from '../DataGridMultiSelect'; +import ListActionsPopover from '../ListActionsPopover'; + +import { hasPermission } from '../../helpers/userPermissions'; + +type Params = (id: string) => void; + +export const loadColumns = async ( + onDelete: Params, + entityName: string, + + user, +) => { + async function callOptionsApi(entityName: string) { + if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; + + try { + const data = await axios(`/${entityName}/autocomplete?limit=100`); + return data.data; + } catch (error) { + console.log(error); + return []; + } + } + + const hasUpdatePermission = hasPermission(user, 'UPDATE_MCS_PYQ'); + + return [ + { + field: 'actions', + type: 'actions', + minWidth: 30, + headerClassName: 'datagrid--header', + cellClassName: 'datagrid--cell', + getActions: (params: GridRowParams) => { + return [ +
    + +
    , + ]; + }, + }, + ]; +}; diff --git a/frontend/src/components/Posts/CardPosts.tsx b/frontend/src/components/Posts/CardPosts.tsx deleted file mode 100644 index a6f5e07..0000000 --- a/frontend/src/components/Posts/CardPosts.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import React from 'react'; -import ImageField from '../ImageField'; -import ListActionsPopover from '../ListActionsPopover'; -import { useAppSelector } from '../../stores/hooks'; -import dataFormatter from '../../helpers/dataFormatter'; -import { Pagination } from '../Pagination'; -import { saveFile } from '../../helpers/fileSaver'; -import LoadingSpinner from '../LoadingSpinner'; -import Link from 'next/link'; - -import { hasPermission } from '../../helpers/userPermissions'; - -type Props = { - posts: any[]; - loading: boolean; - onDelete: (id: string) => void; - currentPage: number; - numPages: number; - onPageChange: (page: number) => void; -}; - -const CardPosts = ({ - posts, - loading, - onDelete, - currentPage, - numPages, - onPageChange, -}: Props) => { - const asideScrollbarsStyle = useAppSelector( - (state) => state.style.asideScrollbarsStyle, - ); - const bgColor = useAppSelector((state) => state.style.cardsColor); - const darkMode = useAppSelector((state) => state.style.darkMode); - const corners = useAppSelector((state) => state.style.corners); - const focusRing = useAppSelector((state) => state.style.focusRingColor); - - const currentUser = useAppSelector((state) => state.auth.currentUser); - const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_POSTS'); - - return ( -
    - {loading && } -
      - {!loading && - posts.map((item, index) => ( -
    • -
      - - {item.content} - - -
      - -
      -
      -
      -
      -
      - DiscussionBoard -
      -
      -
      - {dataFormatter.discussion_boardsOneListFormatter( - item.discussion_board, - )} -
      -
      -
      - -
      -
      User
      -
      -
      - {dataFormatter.usersOneListFormatter(item.user)} -
      -
      -
      - -
      -
      - Content -
      -
      -
      - {item.content} -
      -
      -
      - -
      -
      - PostedAt -
      -
      -
      - {dataFormatter.dateTimeFormatter(item.posted_at)} -
      -
      -
      -
      -
    • - ))} - {!loading && posts.length === 0 && ( -
      -

      No data to display

      -
      - )} -
    -
    - -
    -
    - ); -}; - -export default CardPosts; diff --git a/frontend/src/components/Posts/ListPosts.tsx b/frontend/src/components/Posts/ListPosts.tsx deleted file mode 100644 index 58e17e8..0000000 --- a/frontend/src/components/Posts/ListPosts.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import React from 'react'; -import CardBox from '../CardBox'; -import ImageField from '../ImageField'; -import dataFormatter from '../../helpers/dataFormatter'; -import { saveFile } from '../../helpers/fileSaver'; -import ListActionsPopover from '../ListActionsPopover'; -import { useAppSelector } from '../../stores/hooks'; -import { Pagination } from '../Pagination'; -import LoadingSpinner from '../LoadingSpinner'; -import Link from 'next/link'; - -import { hasPermission } from '../../helpers/userPermissions'; - -type Props = { - posts: any[]; - loading: boolean; - onDelete: (id: string) => void; - currentPage: number; - numPages: number; - onPageChange: (page: number) => void; -}; - -const ListPosts = ({ - posts, - loading, - onDelete, - currentPage, - numPages, - onPageChange, -}: Props) => { - const currentUser = useAppSelector((state) => state.auth.currentUser); - const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_POSTS'); - - const corners = useAppSelector((state) => state.style.corners); - const bgColor = useAppSelector((state) => state.style.cardsColor); - - return ( - <> -
    - {loading && } - {!loading && - posts.map((item) => ( -
    - -
    - dark:divide-dark-700 overflow-x-auto' - } - > -
    -

    - DiscussionBoard -

    -

    - {dataFormatter.discussion_boardsOneListFormatter( - item.discussion_board, - )} -

    -
    - -
    -

    User

    -

    - {dataFormatter.usersOneListFormatter(item.user)} -

    -
    - -
    -

    Content

    -

    {item.content}

    -
    - -
    -

    PostedAt

    -

    - {dataFormatter.dateTimeFormatter(item.posted_at)} -

    -
    - - -
    -
    -
    - ))} - {!loading && posts.length === 0 && ( -
    -

    No data to display

    -
    - )} -
    -
    - -
    - - ); -}; - -export default ListPosts; diff --git a/frontend/src/components/Posts/TablePosts.tsx b/frontend/src/components/Posts/TablePosts.tsx deleted file mode 100644 index ce1cd1f..0000000 --- a/frontend/src/components/Posts/TablePosts.tsx +++ /dev/null @@ -1,497 +0,0 @@ -import React, { useEffect, useState, useMemo } from 'react'; -import { createPortal } from 'react-dom'; -import { ToastContainer, toast } from 'react-toastify'; -import BaseButton from '../BaseButton'; -import CardBoxModal from '../CardBoxModal'; -import CardBox from '../CardBox'; -import { - fetch, - update, - deleteItem, - setRefetch, - deleteItemsByIds, -} from '../../stores/posts/postsSlice'; -import { useAppDispatch, useAppSelector } from '../../stores/hooks'; -import { useRouter } from 'next/router'; -import { Field, Form, Formik } from 'formik'; -import { DataGrid, GridColDef } from '@mui/x-data-grid'; -import { loadColumns } from './configurePostsCols'; -import _ from 'lodash'; -import dataFormatter from '../../helpers/dataFormatter'; -import { dataGridStyles } from '../../styles'; - -import ListPosts from './ListPosts'; - -const perPage = 10; - -const TableSamplePosts = ({ - filterItems, - setFilterItems, - filters, - showGrid, -}) => { - const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); - - const dispatch = useAppDispatch(); - const router = useRouter(); - - const pagesList = []; - const [id, setId] = useState(null); - const [currentPage, setCurrentPage] = useState(0); - const [filterRequest, setFilterRequest] = React.useState(''); - const [columns, setColumns] = useState([]); - const [selectedRows, setSelectedRows] = useState([]); - const [sortModel, setSortModel] = useState([ - { - field: '', - sort: 'desc', - }, - ]); - - const { - posts, - loading, - count, - notify: postsNotify, - refetch, - } = useAppSelector((state) => state.posts); - const { currentUser } = useAppSelector((state) => state.auth); - const focusRing = useAppSelector((state) => state.style.focusRingColor); - const bgColor = useAppSelector((state) => state.style.bgLayoutColor); - const corners = useAppSelector((state) => state.style.corners); - const numPages = - Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); - for (let i = 0; i < numPages; i++) { - pagesList.push(i); - } - - const loadData = async (page = currentPage, request = filterRequest) => { - if (page !== currentPage) setCurrentPage(page); - if (request !== filterRequest) setFilterRequest(request); - const { sort, field } = sortModel[0]; - - const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; - dispatch(fetch({ limit: perPage, page, query })); - }; - - useEffect(() => { - if (postsNotify.showNotification) { - notify(postsNotify.typeNotification, postsNotify.textNotification); - } - }, [postsNotify.showNotification]); - - useEffect(() => { - if (!currentUser) return; - loadData(); - }, [sortModel, currentUser]); - - useEffect(() => { - if (refetch) { - loadData(0); - dispatch(setRefetch(false)); - } - }, [refetch, dispatch]); - - const [isModalInfoActive, setIsModalInfoActive] = useState(false); - const [isModalTrashActive, setIsModalTrashActive] = useState(false); - - const handleModalAction = () => { - setIsModalInfoActive(false); - setIsModalTrashActive(false); - }; - - const handleDeleteModalAction = (id: string) => { - setId(id); - setIsModalTrashActive(true); - }; - const handleDeleteAction = async () => { - if (id) { - await dispatch(deleteItem(id)); - await loadData(0); - setIsModalTrashActive(false); - } - }; - - const generateFilterRequests = useMemo(() => { - let request = '&'; - filterItems.forEach((item) => { - const isRangeFilter = filters.find( - (filter) => - filter.title === item.fields.selectedField && - (filter.number || filter.date), - ); - - if (isRangeFilter) { - const from = item.fields.filterValueFrom; - const to = item.fields.filterValueTo; - if (from) { - request += `${item.fields.selectedField}Range=${from}&`; - } - if (to) { - request += `${item.fields.selectedField}Range=${to}&`; - } - } else { - const value = item.fields.filterValue; - if (value) { - request += `${item.fields.selectedField}=${value}&`; - } - } - }); - return request; - }, [filterItems, filters]); - - const deleteFilter = (value) => { - const newItems = filterItems.filter((item) => item.id !== value); - - if (newItems.length) { - setFilterItems(newItems); - } else { - loadData(0, ''); - - setFilterItems(newItems); - } - }; - - const handleSubmit = () => { - loadData(0, generateFilterRequests); - }; - - const handleChange = (id) => (e) => { - const value = e.target.value; - const name = e.target.name; - - setFilterItems( - filterItems.map((item) => { - if (item.id !== id) return item; - if (name === 'selectedField') return { id, fields: { [name]: value } }; - - return { id, fields: { ...item.fields, [name]: value } }; - }), - ); - }; - - const handleReset = () => { - setFilterItems([]); - loadData(0, ''); - }; - - const onPageChange = (page: number) => { - loadData(page); - setCurrentPage(page); - }; - - useEffect(() => { - if (!currentUser) return; - - loadColumns(handleDeleteModalAction, `posts`, currentUser).then((newCols) => - setColumns(newCols), - ); - }, [currentUser]); - - const handleTableSubmit = async (id: string, data) => { - if (!_.isEmpty(data)) { - await dispatch(update({ id, data })) - .unwrap() - .then((res) => res) - .catch((err) => { - throw new Error(err); - }); - } - }; - - const onDeleteRows = async (selectedRows) => { - await dispatch(deleteItemsByIds(selectedRows)); - await loadData(0); - }; - - const controlClasses = - 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + - ` ${bgColor} ${focusRing} ${corners} ` + - 'dark:bg-slate-800 border'; - - const dataGrid = ( -
    - `datagrid--row`} - rows={posts ?? []} - columns={columns} - initialState={{ - pagination: { - paginationModel: { - pageSize: 10, - }, - }, - }} - disableRowSelectionOnClick - onProcessRowUpdateError={(params) => { - console.log('Error', params); - }} - processRowUpdate={async (newRow, oldRow) => { - const data = dataFormatter.dataGridEditFormatter(newRow); - - try { - await handleTableSubmit(newRow.id, data); - return newRow; - } catch { - return oldRow; - } - }} - sortingMode={'server'} - checkboxSelection - onRowSelectionModelChange={(ids) => { - setSelectedRows(ids); - }} - onSortModelChange={(params) => { - params.length - ? setSortModel(params) - : setSortModel([{ field: '', sort: 'desc' }]); - }} - rowCount={count} - pageSizeOptions={[10]} - paginationMode={'server'} - loading={loading} - onPaginationModelChange={(params) => { - onPageChange(params.page); - }} - /> -
    - ); - - return ( - <> - {filterItems && Array.isArray(filterItems) && filterItems.length ? ( - - null} - > -
    - <> - {filterItems && - filterItems.map((filterItem) => { - return ( -
    -
    -
    - Filter -
    - - {filters.map((selectOption) => ( - - ))} - -
    - {filters.find( - (filter) => - filter.title === filterItem?.fields?.selectedField, - )?.type === 'enum' ? ( -
    -
    Value
    - - - {filters - .find( - (filter) => - filter.title === - filterItem?.fields?.selectedField, - ) - ?.options?.map((option) => ( - - ))} - -
    - ) : filters.find( - (filter) => - filter.title === - filterItem?.fields?.selectedField, - )?.number ? ( -
    -
    -
    - From -
    - -
    -
    -
    - To -
    - -
    -
    - ) : filters.find( - (filter) => - filter.title === - filterItem?.fields?.selectedField, - )?.date ? ( -
    -
    -
    - From -
    - -
    -
    -
    - To -
    - -
    -
    - ) : ( -
    -
    - Contains -
    - -
    - )} -
    -
    - Action -
    - { - deleteFilter(filterItem.id); - }} - /> -
    -
    - ); - })} -
    - - -
    - -
    -
    -
    - ) : null} - -

    Are you sure you want to delete this item?

    -
    - - {posts && Array.isArray(posts) && !showGrid && ( - - )} - - {showGrid && dataGrid} - - {selectedRows.length > 0 && - createPortal( - onDeleteRows(selectedRows)} - />, - document.getElementById('delete-rows-button'), - )} - - - ); -}; - -export default TableSamplePosts; diff --git a/frontend/src/components/Posts/configurePostsCols.tsx b/frontend/src/components/Posts/configurePostsCols.tsx deleted file mode 100644 index 8f7ce58..0000000 --- a/frontend/src/components/Posts/configurePostsCols.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import React from 'react'; -import BaseIcon from '../BaseIcon'; -import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; -import axios from 'axios'; -import { - GridActionsCellItem, - GridRowParams, - GridValueGetterParams, -} from '@mui/x-data-grid'; -import ImageField from '../ImageField'; -import { saveFile } from '../../helpers/fileSaver'; -import dataFormatter from '../../helpers/dataFormatter'; -import DataGridMultiSelect from '../DataGridMultiSelect'; -import ListActionsPopover from '../ListActionsPopover'; - -import { hasPermission } from '../../helpers/userPermissions'; - -type Params = (id: string) => void; - -export const loadColumns = async ( - onDelete: Params, - entityName: string, - - user, -) => { - async function callOptionsApi(entityName: string) { - if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; - - try { - const data = await axios(`/${entityName}/autocomplete?limit=100`); - return data.data; - } catch (error) { - console.log(error); - return []; - } - } - - const hasUpdatePermission = hasPermission(user, 'UPDATE_POSTS'); - - return [ - { - field: 'discussion_board', - headerName: 'DiscussionBoard', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - sortable: false, - type: 'singleSelect', - getOptionValue: (value: any) => value?.id, - getOptionLabel: (value: any) => value?.label, - valueOptions: await callOptionsApi('discussion_boards'), - valueGetter: (params: GridValueGetterParams) => - params?.value?.id ?? params?.value, - }, - - { - field: 'user', - headerName: 'User', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - sortable: false, - type: 'singleSelect', - getOptionValue: (value: any) => value?.id, - getOptionLabel: (value: any) => value?.label, - valueOptions: await callOptionsApi('users'), - valueGetter: (params: GridValueGetterParams) => - params?.value?.id ?? params?.value, - }, - - { - field: 'content', - headerName: 'Content', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - }, - - { - field: 'posted_at', - headerName: 'PostedAt', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - type: 'dateTime', - valueGetter: (params: GridValueGetterParams) => - new Date(params.row.posted_at), - }, - - { - field: 'actions', - type: 'actions', - minWidth: 30, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - getActions: (params: GridRowParams) => { - return [ -
    - -
    , - ]; - }, - }, - ]; -}; diff --git a/frontend/src/components/Students/TableStudents.tsx b/frontend/src/components/Students/TableStudents.tsx deleted file mode 100644 index 9d377eb..0000000 --- a/frontend/src/components/Students/TableStudents.tsx +++ /dev/null @@ -1,497 +0,0 @@ -import React, { useEffect, useState, useMemo } from 'react'; -import { createPortal } from 'react-dom'; -import { ToastContainer, toast } from 'react-toastify'; -import BaseButton from '../BaseButton'; -import CardBoxModal from '../CardBoxModal'; -import CardBox from '../CardBox'; -import { - fetch, - update, - deleteItem, - setRefetch, - deleteItemsByIds, -} from '../../stores/students/studentsSlice'; -import { useAppDispatch, useAppSelector } from '../../stores/hooks'; -import { useRouter } from 'next/router'; -import { Field, Form, Formik } from 'formik'; -import { DataGrid, GridColDef } from '@mui/x-data-grid'; -import { loadColumns } from './configureStudentsCols'; -import _ from 'lodash'; -import dataFormatter from '../../helpers/dataFormatter'; -import { dataGridStyles } from '../../styles'; - -import ListStudents from './ListStudents'; - -const perPage = 10; - -const TableSampleStudents = ({ - filterItems, - setFilterItems, - filters, - showGrid, -}) => { - const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' }); - - const dispatch = useAppDispatch(); - const router = useRouter(); - - const pagesList = []; - const [id, setId] = useState(null); - const [currentPage, setCurrentPage] = useState(0); - const [filterRequest, setFilterRequest] = React.useState(''); - const [columns, setColumns] = useState([]); - const [selectedRows, setSelectedRows] = useState([]); - const [sortModel, setSortModel] = useState([ - { - field: '', - sort: 'desc', - }, - ]); - - const { - students, - loading, - count, - notify: studentsNotify, - refetch, - } = useAppSelector((state) => state.students); - const { currentUser } = useAppSelector((state) => state.auth); - const focusRing = useAppSelector((state) => state.style.focusRingColor); - const bgColor = useAppSelector((state) => state.style.bgLayoutColor); - const corners = useAppSelector((state) => state.style.corners); - const numPages = - Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage); - for (let i = 0; i < numPages; i++) { - pagesList.push(i); - } - - const loadData = async (page = currentPage, request = filterRequest) => { - if (page !== currentPage) setCurrentPage(page); - if (request !== filterRequest) setFilterRequest(request); - const { sort, field } = sortModel[0]; - - const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`; - dispatch(fetch({ limit: perPage, page, query })); - }; - - useEffect(() => { - if (studentsNotify.showNotification) { - notify(studentsNotify.typeNotification, studentsNotify.textNotification); - } - }, [studentsNotify.showNotification]); - - useEffect(() => { - if (!currentUser) return; - loadData(); - }, [sortModel, currentUser]); - - useEffect(() => { - if (refetch) { - loadData(0); - dispatch(setRefetch(false)); - } - }, [refetch, dispatch]); - - const [isModalInfoActive, setIsModalInfoActive] = useState(false); - const [isModalTrashActive, setIsModalTrashActive] = useState(false); - - const handleModalAction = () => { - setIsModalInfoActive(false); - setIsModalTrashActive(false); - }; - - const handleDeleteModalAction = (id: string) => { - setId(id); - setIsModalTrashActive(true); - }; - const handleDeleteAction = async () => { - if (id) { - await dispatch(deleteItem(id)); - await loadData(0); - setIsModalTrashActive(false); - } - }; - - const generateFilterRequests = useMemo(() => { - let request = '&'; - filterItems.forEach((item) => { - const isRangeFilter = filters.find( - (filter) => - filter.title === item.fields.selectedField && - (filter.number || filter.date), - ); - - if (isRangeFilter) { - const from = item.fields.filterValueFrom; - const to = item.fields.filterValueTo; - if (from) { - request += `${item.fields.selectedField}Range=${from}&`; - } - if (to) { - request += `${item.fields.selectedField}Range=${to}&`; - } - } else { - const value = item.fields.filterValue; - if (value) { - request += `${item.fields.selectedField}=${value}&`; - } - } - }); - return request; - }, [filterItems, filters]); - - const deleteFilter = (value) => { - const newItems = filterItems.filter((item) => item.id !== value); - - if (newItems.length) { - setFilterItems(newItems); - } else { - loadData(0, ''); - - setFilterItems(newItems); - } - }; - - const handleSubmit = () => { - loadData(0, generateFilterRequests); - }; - - const handleChange = (id) => (e) => { - const value = e.target.value; - const name = e.target.name; - - setFilterItems( - filterItems.map((item) => { - if (item.id !== id) return item; - if (name === 'selectedField') return { id, fields: { [name]: value } }; - - return { id, fields: { ...item.fields, [name]: value } }; - }), - ); - }; - - const handleReset = () => { - setFilterItems([]); - loadData(0, ''); - }; - - const onPageChange = (page: number) => { - loadData(page); - setCurrentPage(page); - }; - - useEffect(() => { - if (!currentUser) return; - - loadColumns(handleDeleteModalAction, `students`, currentUser).then( - (newCols) => setColumns(newCols), - ); - }, [currentUser]); - - const handleTableSubmit = async (id: string, data) => { - if (!_.isEmpty(data)) { - await dispatch(update({ id, data })) - .unwrap() - .then((res) => res) - .catch((err) => { - throw new Error(err); - }); - } - }; - - const onDeleteRows = async (selectedRows) => { - await dispatch(deleteItemsByIds(selectedRows)); - await loadData(0); - }; - - const controlClasses = - 'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' + - ` ${bgColor} ${focusRing} ${corners} ` + - 'dark:bg-slate-800 border'; - - const dataGrid = ( -
    - `datagrid--row`} - rows={students ?? []} - columns={columns} - initialState={{ - pagination: { - paginationModel: { - pageSize: 10, - }, - }, - }} - disableRowSelectionOnClick - onProcessRowUpdateError={(params) => { - console.log('Error', params); - }} - processRowUpdate={async (newRow, oldRow) => { - const data = dataFormatter.dataGridEditFormatter(newRow); - - try { - await handleTableSubmit(newRow.id, data); - return newRow; - } catch { - return oldRow; - } - }} - sortingMode={'server'} - checkboxSelection - onRowSelectionModelChange={(ids) => { - setSelectedRows(ids); - }} - onSortModelChange={(params) => { - params.length - ? setSortModel(params) - : setSortModel([{ field: '', sort: 'desc' }]); - }} - rowCount={count} - pageSizeOptions={[10]} - paginationMode={'server'} - loading={loading} - onPaginationModelChange={(params) => { - onPageChange(params.page); - }} - /> -
    - ); - - return ( - <> - {filterItems && Array.isArray(filterItems) && filterItems.length ? ( - - null} - > -
    - <> - {filterItems && - filterItems.map((filterItem) => { - return ( -
    -
    -
    - Filter -
    - - {filters.map((selectOption) => ( - - ))} - -
    - {filters.find( - (filter) => - filter.title === filterItem?.fields?.selectedField, - )?.type === 'enum' ? ( -
    -
    Value
    - - - {filters - .find( - (filter) => - filter.title === - filterItem?.fields?.selectedField, - ) - ?.options?.map((option) => ( - - ))} - -
    - ) : filters.find( - (filter) => - filter.title === - filterItem?.fields?.selectedField, - )?.number ? ( -
    -
    -
    - From -
    - -
    -
    -
    - To -
    - -
    -
    - ) : filters.find( - (filter) => - filter.title === - filterItem?.fields?.selectedField, - )?.date ? ( -
    -
    -
    - From -
    - -
    -
    -
    - To -
    - -
    -
    - ) : ( -
    -
    - Contains -
    - -
    - )} -
    -
    - Action -
    - { - deleteFilter(filterItem.id); - }} - /> -
    -
    - ); - })} -
    - - -
    - -
    -
    -
    - ) : null} - -

    Are you sure you want to delete this item?

    -
    - - {students && Array.isArray(students) && !showGrid && ( - - )} - - {showGrid && dataGrid} - - {selectedRows.length > 0 && - createPortal( - onDeleteRows(selectedRows)} - />, - document.getElementById('delete-rows-button'), - )} - - - ); -}; - -export default TableSampleStudents; diff --git a/frontend/src/components/Students/configureStudentsCols.tsx b/frontend/src/components/Students/configureStudentsCols.tsx deleted file mode 100644 index cc26207..0000000 --- a/frontend/src/components/Students/configureStudentsCols.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import React from 'react'; -import BaseIcon from '../BaseIcon'; -import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js'; -import axios from 'axios'; -import { - GridActionsCellItem, - GridRowParams, - GridValueGetterParams, -} from '@mui/x-data-grid'; -import ImageField from '../ImageField'; -import { saveFile } from '../../helpers/fileSaver'; -import dataFormatter from '../../helpers/dataFormatter'; -import DataGridMultiSelect from '../DataGridMultiSelect'; -import ListActionsPopover from '../ListActionsPopover'; - -import { hasPermission } from '../../helpers/userPermissions'; - -type Params = (id: string) => void; - -export const loadColumns = async ( - onDelete: Params, - entityName: string, - - user, -) => { - async function callOptionsApi(entityName: string) { - if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return []; - - try { - const data = await axios(`/${entityName}/autocomplete?limit=100`); - return data.data; - } catch (error) { - console.log(error); - return []; - } - } - - const hasUpdatePermission = hasPermission(user, 'UPDATE_STUDENTS'); - - return [ - { - field: 'user', - headerName: 'User', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: hasUpdatePermission, - - sortable: false, - type: 'singleSelect', - getOptionValue: (value: any) => value?.id, - getOptionLabel: (value: any) => value?.label, - valueOptions: await callOptionsApi('users'), - valueGetter: (params: GridValueGetterParams) => - params?.value?.id ?? params?.value, - }, - - { - field: 'enrollments', - headerName: 'Enrollments', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: false, - sortable: false, - type: 'singleSelect', - valueFormatter: ({ value }) => - dataFormatter.enrollmentsManyListFormatter(value).join(', '), - renderEditCell: (params) => ( - - ), - }, - - { - field: 'grades', - headerName: 'Grades', - flex: 1, - minWidth: 120, - filterable: false, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - - editable: false, - sortable: false, - type: 'singleSelect', - valueFormatter: ({ value }) => - dataFormatter.gradesManyListFormatter(value).join(', '), - renderEditCell: (params) => ( - - ), - }, - - { - field: 'actions', - type: 'actions', - minWidth: 30, - headerClassName: 'datagrid--header', - cellClassName: 'datagrid--cell', - getActions: (params: GridRowParams) => { - return [ -
    - -
    , - ]; - }, - }, - ]; -}; diff --git a/frontend/src/components/WebPageComponents/Footer.tsx b/frontend/src/components/WebPageComponents/Footer.tsx index 583fe3d..14b4b8e 100644 --- a/frontend/src/components/WebPageComponents/Footer.tsx +++ b/frontend/src/components/WebPageComponents/Footer.tsx @@ -17,9 +17,9 @@ export default function WebSiteFooter({ projectName }: WebSiteFooterProps) { const borders = useAppSelector((state) => state.style.borders); const websiteHeder = useAppSelector((state) => state.style.websiteHeder); - const style = FooterStyle.WITH_PAGES; + const style = FooterStyle.WITH_PROJECT_NAME; - const design = FooterDesigns.DESIGN_DIVERSITY; + const design = FooterDesigns.DEFAULT_DESIGN; return (
    item.firstName); - }, - usersOneListFormatter(val) { - if (!val) return ''; - return val.firstName; - }, - usersManyListFormatterEdit(val) { - if (!val || !val.length) return []; - return val.map((item) => { - return { id: item.id, label: item.firstName }; - }); - }, - usersOneListFormatterEdit(val) { - if (!val) return ''; - return { label: val.firstName, id: val.id }; - }, - - coursesManyListFormatter(val) { - if (!val || !val.length) return []; - return val.map((item) => item.title); - }, - coursesOneListFormatter(val) { - if (!val) return ''; - return val.title; - }, - coursesManyListFormatterEdit(val) { - if (!val || !val.length) return []; - return val.map((item) => { - return { id: item.id, label: item.title }; - }); - }, - coursesOneListFormatterEdit(val) { - if (!val) return ''; - return { label: val.title, id: val.id }; - }, - - discussion_boardsManyListFormatter(val) { - if (!val || !val.length) return []; - return val.map((item) => item.topic); - }, - discussion_boardsOneListFormatter(val) { - if (!val) return ''; - return val.topic; - }, - discussion_boardsManyListFormatterEdit(val) { - if (!val || !val.length) return []; - return val.map((item) => { - return { id: item.id, label: item.topic }; - }); - }, - discussion_boardsOneListFormatterEdit(val) { - if (!val) return ''; - return { label: val.topic, id: val.id }; - }, - - enrollmentsManyListFormatter(val) { - if (!val || !val.length) return []; - return val.map((item) => item.course); - }, - enrollmentsOneListFormatter(val) { - if (!val) return ''; - return val.course; - }, - enrollmentsManyListFormatterEdit(val) { - if (!val || !val.length) return []; - return val.map((item) => { - return { id: item.id, label: item.course }; - }); - }, - enrollmentsOneListFormatterEdit(val) { - if (!val) return ''; - return { label: val.course, id: val.id }; - }, - - gradesManyListFormatter(val) { - if (!val || !val.length) return []; - return val.map((item) => item.grade); - }, - gradesOneListFormatter(val) { - if (!val) return ''; - return val.grade; - }, - gradesManyListFormatterEdit(val) { - if (!val || !val.length) return []; - return val.map((item) => { - return { id: item.id, label: item.grade }; - }); - }, - gradesOneListFormatterEdit(val) { - if (!val) return ''; - return { label: val.grade, id: val.id }; - }, - - postsManyListFormatter(val) { - if (!val || !val.length) return []; - return val.map((item) => item.content); - }, - postsOneListFormatter(val) { - if (!val) return ''; - return val.content; - }, - postsManyListFormatterEdit(val) { - if (!val || !val.length) return []; - return val.map((item) => { - return { id: item.id, label: item.content }; - }); - }, - postsOneListFormatterEdit(val) { - if (!val) return ''; - return { label: val.content, id: val.id }; - }, - - studentsManyListFormatter(val) { - if (!val || !val.length) return []; - return val.map((item) => item.user); - }, - studentsOneListFormatter(val) { - if (!val) return ''; - return val.user; - }, - studentsManyListFormatterEdit(val) { - if (!val || !val.length) return []; - return val.map((item) => { - return { id: item.id, label: item.user }; - }); - }, - studentsOneListFormatterEdit(val) { - if (!val) return ''; - return { label: val.user, id: val.id }; - }, - rolesManyListFormatter(val) { if (!val || !val.length) return []; return val.map((item) => item.name); diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 9b8211c..c10a473 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -16,94 +16,6 @@ const menuAside: MenuAsideItem[] = [ icon: icon.mdiAccountGroup ?? icon.mdiTable, permissions: 'READ_USERS', }, - { - href: '/analytics/analytics-list', - label: 'Analytics', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: - 'mdiChartLine' in icon - ? icon['mdiChartLine' as keyof typeof icon] - : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_ANALYTICS', - }, - { - href: '/courses/courses-list', - label: 'Courses', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: - 'mdiBookOpenPageVariant' in icon - ? icon['mdiBookOpenPageVariant' as keyof typeof icon] - : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_COURSES', - }, - { - href: '/discussion_boards/discussion_boards-list', - label: 'Discussion boards', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: - 'mdiForumOutline' in icon - ? icon['mdiForumOutline' as keyof typeof icon] - : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_DISCUSSION_BOARDS', - }, - { - href: '/enrollments/enrollments-list', - label: 'Enrollments', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: - 'mdiClipboardCheckOutline' in icon - ? icon['mdiClipboardCheckOutline' as keyof typeof icon] - : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_ENROLLMENTS', - }, - { - href: '/grades/grades-list', - label: 'Grades', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: - 'mdiChartBar' in icon - ? icon['mdiChartBar' as keyof typeof icon] - : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_GRADES', - }, - { - href: '/instructors/instructors-list', - label: 'Instructors', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: - 'mdiTeach' in icon - ? icon['mdiTeach' as keyof typeof icon] - : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_INSTRUCTORS', - }, - { - href: '/posts/posts-list', - label: 'Posts', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: - 'mdiCommentTextOutline' in icon - ? icon['mdiCommentTextOutline' as keyof typeof icon] - : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_POSTS', - }, - { - href: '/students/students-list', - label: 'Students', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - icon: - 'mdiSchool' in icon - ? icon['mdiSchool' as keyof typeof icon] - : icon.mdiTable ?? icon.mdiTable, - permissions: 'READ_STUDENTS', - }, { href: '/roles/roles-list', label: 'Roles', @@ -120,6 +32,22 @@ const menuAside: MenuAsideItem[] = [ icon: icon.mdiShieldAccountOutline ?? icon.mdiTable, permissions: 'READ_PERMISSIONS', }, + { + href: '/mcs_pyq/mcs_pyq-list', + label: 'Mcs pyq', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_MCS_PYQ', + }, + { + href: '/course/course-list', + label: 'Course', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + icon: icon.mdiTable ?? icon.mdiTable, + permissions: 'READ_COURSE', + }, { href: '/profile', label: 'Profile', diff --git a/frontend/src/pages/analytics/[analyticsId].tsx b/frontend/src/pages/analytics/[analyticsId].tsx deleted file mode 100644 index 20e89ea..0000000 --- a/frontend/src/pages/analytics/[analyticsId].tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; -import Head from 'next/head'; -import React, { ReactElement, useEffect, useState } from 'react'; -import DatePicker from 'react-datepicker'; -import 'react-datepicker/dist/react-datepicker.css'; -import dayjs from 'dayjs'; - -import CardBox from '../../components/CardBox'; -import LayoutAuthenticated from '../../layouts/Authenticated'; -import SectionMain from '../../components/SectionMain'; -import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; -import { getPageTitle } from '../../config'; - -import { Field, Form, Formik } from 'formik'; -import FormField from '../../components/FormField'; -import BaseDivider from '../../components/BaseDivider'; -import BaseButtons from '../../components/BaseButtons'; -import BaseButton from '../../components/BaseButton'; -import FormCheckRadio from '../../components/FormCheckRadio'; -import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; -import FormFilePicker from '../../components/FormFilePicker'; -import FormImagePicker from '../../components/FormImagePicker'; -import { SelectField } from '../../components/SelectField'; -import { SelectFieldMany } from '../../components/SelectFieldMany'; -import { SwitchField } from '../../components/SwitchField'; -import { RichTextField } from '../../components/RichTextField'; - -import { update, fetch } from '../../stores/analytics/analyticsSlice'; -import { useAppDispatch, useAppSelector } from '../../stores/hooks'; -import { useRouter } from 'next/router'; -import { saveFile } from '../../helpers/fileSaver'; -import dataFormatter from '../../helpers/dataFormatter'; -import ImageField from '../../components/ImageField'; - -const EditAnalytics = () => { - const router = useRouter(); - const dispatch = useAppDispatch(); - const initVals = { - course: null, - - student_engagement: '', - - completion_rate: '', - - instructor_performance: '', - }; - const [initialValues, setInitialValues] = useState(initVals); - - const { analytics } = useAppSelector((state) => state.analytics); - - const { analyticsId } = router.query; - - useEffect(() => { - dispatch(fetch({ id: analyticsId })); - }, [analyticsId]); - - useEffect(() => { - if (typeof analytics === 'object') { - setInitialValues(analytics); - } - }, [analytics]); - - useEffect(() => { - if (typeof analytics === 'object') { - const newInitialVal = { ...initVals }; - - Object.keys(initVals).forEach( - (el) => (newInitialVal[el] = analytics[el]), - ); - - setInitialValues(newInitialVal); - } - }, [analytics]); - - const handleSubmit = async (data) => { - await dispatch(update({ id: analyticsId, data })); - await router.push('/analytics/analytics-list'); - }; - - return ( - <> - - {getPageTitle('Edit analytics')} - - - - {''} - - - handleSubmit(values)} - > -
    - - - - - - - - - - - - - - - - - - - - - router.push('/analytics/analytics-list')} - /> - - -
    -
    -
    - - ); -}; - -EditAnalytics.getLayout = function getLayout(page: ReactElement) { - return ( - - {page} - - ); -}; - -export default EditAnalytics; diff --git a/frontend/src/pages/analytics/analytics-edit.tsx b/frontend/src/pages/analytics/analytics-edit.tsx deleted file mode 100644 index c70883c..0000000 --- a/frontend/src/pages/analytics/analytics-edit.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; -import Head from 'next/head'; -import React, { ReactElement, useEffect, useState } from 'react'; -import DatePicker from 'react-datepicker'; -import 'react-datepicker/dist/react-datepicker.css'; -import dayjs from 'dayjs'; - -import CardBox from '../../components/CardBox'; -import LayoutAuthenticated from '../../layouts/Authenticated'; -import SectionMain from '../../components/SectionMain'; -import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; -import { getPageTitle } from '../../config'; - -import { Field, Form, Formik } from 'formik'; -import FormField from '../../components/FormField'; -import BaseDivider from '../../components/BaseDivider'; -import BaseButtons from '../../components/BaseButtons'; -import BaseButton from '../../components/BaseButton'; -import FormCheckRadio from '../../components/FormCheckRadio'; -import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; -import FormFilePicker from '../../components/FormFilePicker'; -import FormImagePicker from '../../components/FormImagePicker'; -import { SelectField } from '../../components/SelectField'; -import { SelectFieldMany } from '../../components/SelectFieldMany'; -import { SwitchField } from '../../components/SwitchField'; -import { RichTextField } from '../../components/RichTextField'; - -import { update, fetch } from '../../stores/analytics/analyticsSlice'; -import { useAppDispatch, useAppSelector } from '../../stores/hooks'; -import { useRouter } from 'next/router'; -import { saveFile } from '../../helpers/fileSaver'; -import dataFormatter from '../../helpers/dataFormatter'; -import ImageField from '../../components/ImageField'; - -const EditAnalyticsPage = () => { - const router = useRouter(); - const dispatch = useAppDispatch(); - const initVals = { - course: null, - - student_engagement: '', - - completion_rate: '', - - instructor_performance: '', - }; - const [initialValues, setInitialValues] = useState(initVals); - - const { analytics } = useAppSelector((state) => state.analytics); - - const { id } = router.query; - - useEffect(() => { - dispatch(fetch({ id: id })); - }, [id]); - - useEffect(() => { - if (typeof analytics === 'object') { - setInitialValues(analytics); - } - }, [analytics]); - - useEffect(() => { - if (typeof analytics === 'object') { - const newInitialVal = { ...initVals }; - Object.keys(initVals).forEach( - (el) => (newInitialVal[el] = analytics[el]), - ); - setInitialValues(newInitialVal); - } - }, [analytics]); - - const handleSubmit = async (data) => { - await dispatch(update({ id: id, data })); - await router.push('/analytics/analytics-list'); - }; - - return ( - <> - - {getPageTitle('Edit analytics')} - - - - {''} - - - handleSubmit(values)} - > -
    - - - - - - - - - - - - - - - - - - - - - router.push('/analytics/analytics-list')} - /> - - -
    -
    -
    - - ); -}; - -EditAnalyticsPage.getLayout = function getLayout(page: ReactElement) { - return ( - - {page} - - ); -}; - -export default EditAnalyticsPage; diff --git a/frontend/src/pages/analytics/analytics-list.tsx b/frontend/src/pages/analytics/analytics-list.tsx deleted file mode 100644 index 61d8596..0000000 --- a/frontend/src/pages/analytics/analytics-list.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import { mdiChartTimelineVariant } from '@mdi/js'; -import Head from 'next/head'; -import { uniqueId } from 'lodash'; -import React, { ReactElement, useState } from 'react'; -import CardBox from '../../components/CardBox'; -import LayoutAuthenticated from '../../layouts/Authenticated'; -import SectionMain from '../../components/SectionMain'; -import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; -import { getPageTitle } from '../../config'; -import TableAnalytics from '../../components/Analytics/TableAnalytics'; -import BaseButton from '../../components/BaseButton'; -import axios from 'axios'; -import Link from 'next/link'; -import { useAppDispatch, useAppSelector } from '../../stores/hooks'; -import CardBoxModal from '../../components/CardBoxModal'; -import DragDropFilePicker from '../../components/DragDropFilePicker'; -import { setRefetch, uploadCsv } from '../../stores/analytics/analyticsSlice'; - -import { hasPermission } from '../../helpers/userPermissions'; - -const AnalyticsTablesPage = () => { - const [filterItems, setFilterItems] = useState([]); - const [csvFile, setCsvFile] = useState(null); - const [isModalActive, setIsModalActive] = useState(false); - const [showTableView, setShowTableView] = useState(false); - - const { currentUser } = useAppSelector((state) => state.auth); - - const dispatch = useAppDispatch(); - - const [filters] = useState([ - { label: 'StudentEngagement', title: 'student_engagement', number: 'true' }, - { label: 'CompletionRate', title: 'completion_rate', number: 'true' }, - { - label: 'InstructorPerformance', - title: 'instructor_performance', - number: 'true', - }, - - { label: 'Course', title: 'course' }, - ]); - - const hasCreatePermission = - currentUser && hasPermission(currentUser, 'CREATE_ANALYTICS'); - - const addFilter = () => { - const newItem = { - id: uniqueId(), - fields: { - filterValue: '', - filterValueFrom: '', - filterValueTo: '', - selectedField: '', - }, - }; - newItem.fields.selectedField = filters[0].title; - setFilterItems([...filterItems, newItem]); - }; - - const getAnalyticsCSV = async () => { - const response = await axios({ - url: '/analytics?filetype=csv', - method: 'GET', - responseType: 'blob', - }); - const type = response.headers['content-type']; - const blob = new Blob([response.data], { type: type }); - const link = document.createElement('a'); - link.href = window.URL.createObjectURL(blob); - link.download = 'analyticsCSV.csv'; - link.click(); - }; - - const onModalConfirm = async () => { - if (!csvFile) return; - await dispatch(uploadCsv(csvFile)); - dispatch(setRefetch(true)); - setCsvFile(null); - setIsModalActive(false); - }; - - const onModalCancel = () => { - setCsvFile(null); - setIsModalActive(false); - }; - - return ( - <> - - {getPageTitle('Analytics')} - - - - {''} - - - {hasCreatePermission && ( - - )} - - - - - {hasCreatePermission && ( - setIsModalActive(true)} - /> - )} - -
    -
    -
    -
    - - - - -
    - - - - - ); -}; - -AnalyticsTablesPage.getLayout = function getLayout(page: ReactElement) { - return ( - - {page} - - ); -}; - -export default AnalyticsTablesPage; diff --git a/frontend/src/pages/analytics/analytics-new.tsx b/frontend/src/pages/analytics/analytics-new.tsx deleted file mode 100644 index 3daf1bd..0000000 --- a/frontend/src/pages/analytics/analytics-new.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { - mdiAccount, - mdiChartTimelineVariant, - mdiMail, - mdiUpload, -} from '@mdi/js'; -import Head from 'next/head'; -import React, { ReactElement } from 'react'; -import CardBox from '../../components/CardBox'; -import LayoutAuthenticated from '../../layouts/Authenticated'; -import SectionMain from '../../components/SectionMain'; -import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; -import { getPageTitle } from '../../config'; - -import { Field, Form, Formik } from 'formik'; -import FormField from '../../components/FormField'; -import BaseDivider from '../../components/BaseDivider'; -import BaseButtons from '../../components/BaseButtons'; -import BaseButton from '../../components/BaseButton'; -import FormCheckRadio from '../../components/FormCheckRadio'; -import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; -import FormFilePicker from '../../components/FormFilePicker'; -import FormImagePicker from '../../components/FormImagePicker'; -import { SwitchField } from '../../components/SwitchField'; - -import { SelectField } from '../../components/SelectField'; -import { SelectFieldMany } from '../../components/SelectFieldMany'; -import { RichTextField } from '../../components/RichTextField'; - -import { create } from '../../stores/analytics/analyticsSlice'; -import { useAppDispatch } from '../../stores/hooks'; -import { useRouter } from 'next/router'; -import moment from 'moment'; - -const initialValues = { - course: '', - - student_engagement: '', - - completion_rate: '', - - instructor_performance: '', -}; - -const AnalyticsNew = () => { - const router = useRouter(); - const dispatch = useAppDispatch(); - - const handleSubmit = async (data) => { - await dispatch(create(data)); - await router.push('/analytics/analytics-list'); - }; - return ( - <> - - {getPageTitle('New Item')} - - - - {''} - - - handleSubmit(values)} - > -
    - - - - - - - - - - - - - - - - - - - - - router.push('/analytics/analytics-list')} - /> - - -
    -
    -
    - - ); -}; - -AnalyticsNew.getLayout = function getLayout(page: ReactElement) { - return ( - - {page} - - ); -}; - -export default AnalyticsNew; diff --git a/frontend/src/pages/analytics/analytics-table.tsx b/frontend/src/pages/analytics/analytics-table.tsx deleted file mode 100644 index e4c1f0a..0000000 --- a/frontend/src/pages/analytics/analytics-table.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import { mdiChartTimelineVariant } from '@mdi/js'; -import Head from 'next/head'; -import { uniqueId } from 'lodash'; -import React, { ReactElement, useState } from 'react'; -import CardBox from '../../components/CardBox'; -import LayoutAuthenticated from '../../layouts/Authenticated'; -import SectionMain from '../../components/SectionMain'; -import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; -import { getPageTitle } from '../../config'; -import TableAnalytics from '../../components/Analytics/TableAnalytics'; -import BaseButton from '../../components/BaseButton'; -import axios from 'axios'; -import Link from 'next/link'; -import { useAppDispatch, useAppSelector } from '../../stores/hooks'; -import CardBoxModal from '../../components/CardBoxModal'; -import DragDropFilePicker from '../../components/DragDropFilePicker'; -import { setRefetch, uploadCsv } from '../../stores/analytics/analyticsSlice'; - -import { hasPermission } from '../../helpers/userPermissions'; - -const AnalyticsTablesPage = () => { - const [filterItems, setFilterItems] = useState([]); - const [csvFile, setCsvFile] = useState(null); - const [isModalActive, setIsModalActive] = useState(false); - const [showTableView, setShowTableView] = useState(false); - - const { currentUser } = useAppSelector((state) => state.auth); - - const dispatch = useAppDispatch(); - - const [filters] = useState([ - { label: 'StudentEngagement', title: 'student_engagement', number: 'true' }, - { label: 'CompletionRate', title: 'completion_rate', number: 'true' }, - { - label: 'InstructorPerformance', - title: 'instructor_performance', - number: 'true', - }, - - { label: 'Course', title: 'course' }, - ]); - - const hasCreatePermission = - currentUser && hasPermission(currentUser, 'CREATE_ANALYTICS'); - - const addFilter = () => { - const newItem = { - id: uniqueId(), - fields: { - filterValue: '', - filterValueFrom: '', - filterValueTo: '', - selectedField: '', - }, - }; - newItem.fields.selectedField = filters[0].title; - setFilterItems([...filterItems, newItem]); - }; - - const getAnalyticsCSV = async () => { - const response = await axios({ - url: '/analytics?filetype=csv', - method: 'GET', - responseType: 'blob', - }); - const type = response.headers['content-type']; - const blob = new Blob([response.data], { type: type }); - const link = document.createElement('a'); - link.href = window.URL.createObjectURL(blob); - link.download = 'analyticsCSV.csv'; - link.click(); - }; - - const onModalConfirm = async () => { - if (!csvFile) return; - await dispatch(uploadCsv(csvFile)); - dispatch(setRefetch(true)); - setCsvFile(null); - setIsModalActive(false); - }; - - const onModalCancel = () => { - setCsvFile(null); - setIsModalActive(false); - }; - - return ( - <> - - {getPageTitle('Analytics')} - - - - {''} - - - {hasCreatePermission && ( - - )} - - - - - {hasCreatePermission && ( - setIsModalActive(true)} - /> - )} - -
    -
    - - - Back to table - -
    -
    - - - -
    - - - - - ); -}; - -AnalyticsTablesPage.getLayout = function getLayout(page: ReactElement) { - return ( - - {page} - - ); -}; - -export default AnalyticsTablesPage; diff --git a/frontend/src/pages/instructors/[instructorsId].tsx b/frontend/src/pages/course/[courseId].tsx similarity index 58% rename from frontend/src/pages/instructors/[instructorsId].tsx rename to frontend/src/pages/course/[courseId].tsx index dbde563..4992c3b 100644 --- a/frontend/src/pages/instructors/[instructorsId].tsx +++ b/frontend/src/pages/course/[courseId].tsx @@ -25,65 +25,57 @@ import { SelectFieldMany } from '../../components/SelectFieldMany'; import { SwitchField } from '../../components/SwitchField'; import { RichTextField } from '../../components/RichTextField'; -import { update, fetch } from '../../stores/instructors/instructorsSlice'; +import { update, fetch } from '../../stores/course/courseSlice'; import { useAppDispatch, useAppSelector } from '../../stores/hooks'; import { useRouter } from 'next/router'; import { saveFile } from '../../helpers/fileSaver'; import dataFormatter from '../../helpers/dataFormatter'; import ImageField from '../../components/ImageField'; -const EditInstructors = () => { +const EditCourse = () => { const router = useRouter(); const dispatch = useAppDispatch(); - const initVals = { - user: null, - - qualifications: '', - - courses: [], - }; + const initVals = {}; const [initialValues, setInitialValues] = useState(initVals); - const { instructors } = useAppSelector((state) => state.instructors); + const { course } = useAppSelector((state) => state.course); - const { instructorsId } = router.query; + const { courseId } = router.query; useEffect(() => { - dispatch(fetch({ id: instructorsId })); - }, [instructorsId]); + dispatch(fetch({ id: courseId })); + }, [courseId]); useEffect(() => { - if (typeof instructors === 'object') { - setInitialValues(instructors); + if (typeof course === 'object') { + setInitialValues(course); } - }, [instructors]); + }, [course]); useEffect(() => { - if (typeof instructors === 'object') { + if (typeof course === 'object') { const newInitialVal = { ...initVals }; - Object.keys(initVals).forEach( - (el) => (newInitialVal[el] = instructors[el]), - ); + Object.keys(initVals).forEach((el) => (newInitialVal[el] = course[el])); setInitialValues(newInitialVal); } - }, [instructors]); + }, [course]); const handleSubmit = async (data) => { - await dispatch(update({ id: instructorsId, data })); - await router.push('/instructors/instructors-list'); + await dispatch(update({ id: courseId, data })); + await router.push('/course/course-list'); }; return ( <> - {getPageTitle('Edit instructors')} + {getPageTitle('Edit course')} {''} @@ -95,36 +87,6 @@ const EditInstructors = () => { onSubmit={(values) => handleSubmit(values)} >
    - - - - - - - - - - - - @@ -134,7 +96,7 @@ const EditInstructors = () => { color='danger' outline label='Cancel' - onClick={() => router.push('/instructors/instructors-list')} + onClick={() => router.push('/course/course-list')} /> @@ -145,12 +107,12 @@ const EditInstructors = () => { ); }; -EditInstructors.getLayout = function getLayout(page: ReactElement) { +EditCourse.getLayout = function getLayout(page: ReactElement) { return ( - + {page} ); }; -export default EditInstructors; +export default EditCourse; diff --git a/frontend/src/pages/instructors/instructors-edit.tsx b/frontend/src/pages/course/course-edit.tsx similarity index 61% rename from frontend/src/pages/instructors/instructors-edit.tsx rename to frontend/src/pages/course/course-edit.tsx index 823fc6a..cc9e6a9 100644 --- a/frontend/src/pages/instructors/instructors-edit.tsx +++ b/frontend/src/pages/course/course-edit.tsx @@ -25,26 +25,20 @@ import { SelectFieldMany } from '../../components/SelectFieldMany'; import { SwitchField } from '../../components/SwitchField'; import { RichTextField } from '../../components/RichTextField'; -import { update, fetch } from '../../stores/instructors/instructorsSlice'; +import { update, fetch } from '../../stores/course/courseSlice'; import { useAppDispatch, useAppSelector } from '../../stores/hooks'; import { useRouter } from 'next/router'; import { saveFile } from '../../helpers/fileSaver'; import dataFormatter from '../../helpers/dataFormatter'; import ImageField from '../../components/ImageField'; -const EditInstructorsPage = () => { +const EditCoursePage = () => { const router = useRouter(); const dispatch = useAppDispatch(); - const initVals = { - user: null, - - qualifications: '', - - courses: [], - }; + const initVals = {}; const [initialValues, setInitialValues] = useState(initVals); - const { instructors } = useAppSelector((state) => state.instructors); + const { course } = useAppSelector((state) => state.course); const { id } = router.query; @@ -53,35 +47,33 @@ const EditInstructorsPage = () => { }, [id]); useEffect(() => { - if (typeof instructors === 'object') { - setInitialValues(instructors); + if (typeof course === 'object') { + setInitialValues(course); } - }, [instructors]); + }, [course]); useEffect(() => { - if (typeof instructors === 'object') { + if (typeof course === 'object') { const newInitialVal = { ...initVals }; - Object.keys(initVals).forEach( - (el) => (newInitialVal[el] = instructors[el]), - ); + Object.keys(initVals).forEach((el) => (newInitialVal[el] = course[el])); setInitialValues(newInitialVal); } - }, [instructors]); + }, [course]); const handleSubmit = async (data) => { await dispatch(update({ id: id, data })); - await router.push('/instructors/instructors-list'); + await router.push('/course/course-list'); }; return ( <> - {getPageTitle('Edit instructors')} + {getPageTitle('Edit course')} {''} @@ -93,36 +85,6 @@ const EditInstructorsPage = () => { onSubmit={(values) => handleSubmit(values)} >
    - - - - - - - - - - - - @@ -132,7 +94,7 @@ const EditInstructorsPage = () => { color='danger' outline label='Cancel' - onClick={() => router.push('/instructors/instructors-list')} + onClick={() => router.push('/course/course-list')} /> @@ -143,12 +105,12 @@ const EditInstructorsPage = () => { ); }; -EditInstructorsPage.getLayout = function getLayout(page: ReactElement) { +EditCoursePage.getLayout = function getLayout(page: ReactElement) { return ( - + {page} ); }; -export default EditInstructorsPage; +export default EditCoursePage; diff --git a/frontend/src/pages/posts/posts-list.tsx b/frontend/src/pages/course/course-list.tsx similarity index 78% rename from frontend/src/pages/posts/posts-list.tsx rename to frontend/src/pages/course/course-list.tsx index 5ee657f..2921e22 100644 --- a/frontend/src/pages/posts/posts-list.tsx +++ b/frontend/src/pages/course/course-list.tsx @@ -7,18 +7,18 @@ import LayoutAuthenticated from '../../layouts/Authenticated'; import SectionMain from '../../components/SectionMain'; import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; import { getPageTitle } from '../../config'; -import TablePosts from '../../components/Posts/TablePosts'; +import TableCourse from '../../components/Course/TableCourse'; import BaseButton from '../../components/BaseButton'; import axios from 'axios'; import Link from 'next/link'; import { useAppDispatch, useAppSelector } from '../../stores/hooks'; import CardBoxModal from '../../components/CardBoxModal'; import DragDropFilePicker from '../../components/DragDropFilePicker'; -import { setRefetch, uploadCsv } from '../../stores/posts/postsSlice'; +import { setRefetch, uploadCsv } from '../../stores/course/courseSlice'; import { hasPermission } from '../../helpers/userPermissions'; -const PostsTablesPage = () => { +const CourseTablesPage = () => { const [filterItems, setFilterItems] = useState([]); const [csvFile, setCsvFile] = useState(null); const [isModalActive, setIsModalActive] = useState(false); @@ -28,18 +28,10 @@ const PostsTablesPage = () => { const dispatch = useAppDispatch(); - const [filters] = useState([ - { label: 'Content', title: 'content' }, - - { label: 'PostedAt', title: 'posted_at', date: 'true' }, - - { label: 'DiscussionBoard', title: 'discussion_board' }, - - { label: 'User', title: 'user' }, - ]); + const [filters] = useState([]); const hasCreatePermission = - currentUser && hasPermission(currentUser, 'CREATE_POSTS'); + currentUser && hasPermission(currentUser, 'CREATE_COURSE'); const addFilter = () => { const newItem = { @@ -55,9 +47,9 @@ const PostsTablesPage = () => { setFilterItems([...filterItems, newItem]); }; - const getPostsCSV = async () => { + const getCourseCSV = async () => { const response = await axios({ - url: '/posts?filetype=csv', + url: '/course?filetype=csv', method: 'GET', responseType: 'blob', }); @@ -65,7 +57,7 @@ const PostsTablesPage = () => { const blob = new Blob([response.data], { type: type }); const link = document.createElement('a'); link.href = window.URL.createObjectURL(blob); - link.download = 'postsCSV.csv'; + link.download = 'courseCSV.csv'; link.click(); }; @@ -85,12 +77,12 @@ const PostsTablesPage = () => { return ( <> - {getPageTitle('Posts')} + {getPageTitle('Course')} {''} @@ -99,7 +91,7 @@ const PostsTablesPage = () => { {hasCreatePermission && ( @@ -115,7 +107,7 @@ const PostsTablesPage = () => { className={'mr-3'} color='info' label='Download CSV' - onClick={getPostsCSV} + onClick={getCourseCSV} /> {hasCreatePermission && ( @@ -129,14 +121,10 @@ const PostsTablesPage = () => {
    - -
    - Switch to Table -
    - { ); }; -PostsTablesPage.getLayout = function getLayout(page: ReactElement) { +CourseTablesPage.getLayout = function getLayout(page: ReactElement) { return ( - {page} + {page} ); }; -export default PostsTablesPage; +export default CourseTablesPage; diff --git a/frontend/src/pages/instructors/instructors-new.tsx b/frontend/src/pages/course/course-new.tsx similarity index 65% rename from frontend/src/pages/instructors/instructors-new.tsx rename to frontend/src/pages/course/course-new.tsx index ca94e8f..7f721d4 100644 --- a/frontend/src/pages/instructors/instructors-new.tsx +++ b/frontend/src/pages/course/course-new.tsx @@ -27,26 +27,20 @@ import { SelectField } from '../../components/SelectField'; import { SelectFieldMany } from '../../components/SelectFieldMany'; import { RichTextField } from '../../components/RichTextField'; -import { create } from '../../stores/instructors/instructorsSlice'; +import { create } from '../../stores/course/courseSlice'; import { useAppDispatch } from '../../stores/hooks'; import { useRouter } from 'next/router'; import moment from 'moment'; -const initialValues = { - user: '', +const initialValues = {}; - qualifications: '', - - courses: [], -}; - -const InstructorsNew = () => { +const CourseNew = () => { const router = useRouter(); const dispatch = useAppDispatch(); const handleSubmit = async (data) => { await dispatch(create(data)); - await router.push('/instructors/instructors-list'); + await router.push('/course/course-list'); }; return ( <> @@ -67,34 +61,6 @@ const InstructorsNew = () => { onSubmit={(values) => handleSubmit(values)} >
    - - - - - - - - - - - - @@ -104,7 +70,7 @@ const InstructorsNew = () => { color='danger' outline label='Cancel' - onClick={() => router.push('/instructors/instructors-list')} + onClick={() => router.push('/course/course-list')} /> @@ -115,12 +81,12 @@ const InstructorsNew = () => { ); }; -InstructorsNew.getLayout = function getLayout(page: ReactElement) { +CourseNew.getLayout = function getLayout(page: ReactElement) { return ( - + {page} ); }; -export default InstructorsNew; +export default CourseNew; diff --git a/frontend/src/pages/posts/posts-table.tsx b/frontend/src/pages/course/course-table.tsx similarity index 78% rename from frontend/src/pages/posts/posts-table.tsx rename to frontend/src/pages/course/course-table.tsx index f9d3136..a48026a 100644 --- a/frontend/src/pages/posts/posts-table.tsx +++ b/frontend/src/pages/course/course-table.tsx @@ -7,18 +7,18 @@ import LayoutAuthenticated from '../../layouts/Authenticated'; import SectionMain from '../../components/SectionMain'; import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; import { getPageTitle } from '../../config'; -import TablePosts from '../../components/Posts/TablePosts'; +import TableCourse from '../../components/Course/TableCourse'; import BaseButton from '../../components/BaseButton'; import axios from 'axios'; import Link from 'next/link'; import { useAppDispatch, useAppSelector } from '../../stores/hooks'; import CardBoxModal from '../../components/CardBoxModal'; import DragDropFilePicker from '../../components/DragDropFilePicker'; -import { setRefetch, uploadCsv } from '../../stores/posts/postsSlice'; +import { setRefetch, uploadCsv } from '../../stores/course/courseSlice'; import { hasPermission } from '../../helpers/userPermissions'; -const PostsTablesPage = () => { +const CourseTablesPage = () => { const [filterItems, setFilterItems] = useState([]); const [csvFile, setCsvFile] = useState(null); const [isModalActive, setIsModalActive] = useState(false); @@ -28,18 +28,10 @@ const PostsTablesPage = () => { const dispatch = useAppDispatch(); - const [filters] = useState([ - { label: 'Content', title: 'content' }, - - { label: 'PostedAt', title: 'posted_at', date: 'true' }, - - { label: 'DiscussionBoard', title: 'discussion_board' }, - - { label: 'User', title: 'user' }, - ]); + const [filters] = useState([]); const hasCreatePermission = - currentUser && hasPermission(currentUser, 'CREATE_POSTS'); + currentUser && hasPermission(currentUser, 'CREATE_COURSE'); const addFilter = () => { const newItem = { @@ -55,9 +47,9 @@ const PostsTablesPage = () => { setFilterItems([...filterItems, newItem]); }; - const getPostsCSV = async () => { + const getCourseCSV = async () => { const response = await axios({ - url: '/posts?filetype=csv', + url: '/course?filetype=csv', method: 'GET', responseType: 'blob', }); @@ -65,7 +57,7 @@ const PostsTablesPage = () => { const blob = new Blob([response.data], { type: type }); const link = document.createElement('a'); link.href = window.URL.createObjectURL(blob); - link.download = 'postsCSV.csv'; + link.download = 'courseCSV.csv'; link.click(); }; @@ -85,12 +77,12 @@ const PostsTablesPage = () => { return ( <> - {getPageTitle('Posts')} + {getPageTitle('Course')} {''} @@ -99,7 +91,7 @@ const PostsTablesPage = () => { {hasCreatePermission && ( @@ -115,7 +107,7 @@ const PostsTablesPage = () => { className={'mr-3'} color='info' label='Download CSV' - onClick={getPostsCSV} + onClick={getCourseCSV} /> {hasCreatePermission && ( @@ -128,14 +120,10 @@ const PostsTablesPage = () => {
    - - - Back to list -
    - { ); }; -PostsTablesPage.getLayout = function getLayout(page: ReactElement) { +CourseTablesPage.getLayout = function getLayout(page: ReactElement) { return ( - {page} + {page} ); }; -export default PostsTablesPage; +export default CourseTablesPage; diff --git a/frontend/src/pages/posts/posts-view.tsx b/frontend/src/pages/course/course-view.tsx similarity index 51% rename from frontend/src/pages/posts/posts-view.tsx rename to frontend/src/pages/course/course-view.tsx index 6c6ed03..3480bc1 100644 --- a/frontend/src/pages/posts/posts-view.tsx +++ b/frontend/src/pages/course/course-view.tsx @@ -5,7 +5,7 @@ import 'react-datepicker/dist/react-datepicker.css'; import dayjs from 'dayjs'; import { useAppDispatch, useAppSelector } from '../../stores/hooks'; import { useRouter } from 'next/router'; -import { fetch } from '../../stores/posts/postsSlice'; +import { fetch } from '../../stores/course/courseSlice'; import { saveFile } from '../../helpers/fileSaver'; import dataFormatter from '../../helpers/dataFormatter'; import ImageField from '../../components/ImageField'; @@ -20,10 +20,10 @@ import { mdiChartTimelineVariant } from '@mdi/js'; import { SwitchField } from '../../components/SwitchField'; import FormField from '../../components/FormField'; -const PostsView = () => { +const CourseView = () => { const router = useRouter(); const dispatch = useAppDispatch(); - const { posts } = useAppSelector((state) => state.posts); + const { course } = useAppSelector((state) => state.course); const { id } = router.query; @@ -39,67 +39,27 @@ const PostsView = () => { return ( <> - {getPageTitle('View posts')} + {getPageTitle('View course')} -
    -

    DiscussionBoard

    - -

    {posts?.discussion_board?.topic ?? 'No data'}

    -
    - -
    -

    User

    - -

    {posts?.user?.firstName ?? 'No data'}

    -
    - -
    -

    Content

    - {posts.content ? ( -

    - ) : ( -

    No data

    - )} -
    - - - {posts.posted_at ? ( - - ) : ( -

    No PostedAt

    - )} -
    - router.push('/posts/posts-list')} + onClick={() => router.push('/course/course-list')} />
    @@ -107,10 +67,10 @@ const PostsView = () => { ); }; -PostsView.getLayout = function getLayout(page: ReactElement) { +CourseView.getLayout = function getLayout(page: ReactElement) { return ( - {page} + {page} ); }; -export default PostsView; +export default CourseView; diff --git a/frontend/src/pages/courses/[coursesId].tsx b/frontend/src/pages/courses/[coursesId].tsx deleted file mode 100644 index daa208e..0000000 --- a/frontend/src/pages/courses/[coursesId].tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; -import Head from 'next/head'; -import React, { ReactElement, useEffect, useState } from 'react'; -import DatePicker from 'react-datepicker'; -import 'react-datepicker/dist/react-datepicker.css'; -import dayjs from 'dayjs'; - -import CardBox from '../../components/CardBox'; -import LayoutAuthenticated from '../../layouts/Authenticated'; -import SectionMain from '../../components/SectionMain'; -import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; -import { getPageTitle } from '../../config'; - -import { Field, Form, Formik } from 'formik'; -import FormField from '../../components/FormField'; -import BaseDivider from '../../components/BaseDivider'; -import BaseButtons from '../../components/BaseButtons'; -import BaseButton from '../../components/BaseButton'; -import FormCheckRadio from '../../components/FormCheckRadio'; -import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; -import FormFilePicker from '../../components/FormFilePicker'; -import FormImagePicker from '../../components/FormImagePicker'; -import { SelectField } from '../../components/SelectField'; -import { SelectFieldMany } from '../../components/SelectFieldMany'; -import { SwitchField } from '../../components/SwitchField'; -import { RichTextField } from '../../components/RichTextField'; - -import { update, fetch } from '../../stores/courses/coursesSlice'; -import { useAppDispatch, useAppSelector } from '../../stores/hooks'; -import { useRouter } from 'next/router'; -import { saveFile } from '../../helpers/fileSaver'; -import dataFormatter from '../../helpers/dataFormatter'; -import ImageField from '../../components/ImageField'; - -const EditCourses = () => { - const router = useRouter(); - const dispatch = useAppDispatch(); - const initVals = { - title: '', - - description: '', - - instructors: [], - - students: [], - - discussion_boards: [], - }; - const [initialValues, setInitialValues] = useState(initVals); - - const { courses } = useAppSelector((state) => state.courses); - - const { coursesId } = router.query; - - useEffect(() => { - dispatch(fetch({ id: coursesId })); - }, [coursesId]); - - useEffect(() => { - if (typeof courses === 'object') { - setInitialValues(courses); - } - }, [courses]); - - useEffect(() => { - if (typeof courses === 'object') { - const newInitialVal = { ...initVals }; - - Object.keys(initVals).forEach((el) => (newInitialVal[el] = courses[el])); - - setInitialValues(newInitialVal); - } - }, [courses]); - - const handleSubmit = async (data) => { - await dispatch(update({ id: coursesId, data })); - await router.push('/courses/courses-list'); - }; - - return ( - <> - - {getPageTitle('Edit courses')} - - - - {''} - - - handleSubmit(values)} - > -
    - - - - - - - - - - - - - - - - - - - - - - - - - router.push('/courses/courses-list')} - /> - - -
    -
    -
    - - ); -}; - -EditCourses.getLayout = function getLayout(page: ReactElement) { - return ( - - {page} - - ); -}; - -export default EditCourses; diff --git a/frontend/src/pages/courses/courses-edit.tsx b/frontend/src/pages/courses/courses-edit.tsx deleted file mode 100644 index 28b8b34..0000000 --- a/frontend/src/pages/courses/courses-edit.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; -import Head from 'next/head'; -import React, { ReactElement, useEffect, useState } from 'react'; -import DatePicker from 'react-datepicker'; -import 'react-datepicker/dist/react-datepicker.css'; -import dayjs from 'dayjs'; - -import CardBox from '../../components/CardBox'; -import LayoutAuthenticated from '../../layouts/Authenticated'; -import SectionMain from '../../components/SectionMain'; -import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; -import { getPageTitle } from '../../config'; - -import { Field, Form, Formik } from 'formik'; -import FormField from '../../components/FormField'; -import BaseDivider from '../../components/BaseDivider'; -import BaseButtons from '../../components/BaseButtons'; -import BaseButton from '../../components/BaseButton'; -import FormCheckRadio from '../../components/FormCheckRadio'; -import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; -import FormFilePicker from '../../components/FormFilePicker'; -import FormImagePicker from '../../components/FormImagePicker'; -import { SelectField } from '../../components/SelectField'; -import { SelectFieldMany } from '../../components/SelectFieldMany'; -import { SwitchField } from '../../components/SwitchField'; -import { RichTextField } from '../../components/RichTextField'; - -import { update, fetch } from '../../stores/courses/coursesSlice'; -import { useAppDispatch, useAppSelector } from '../../stores/hooks'; -import { useRouter } from 'next/router'; -import { saveFile } from '../../helpers/fileSaver'; -import dataFormatter from '../../helpers/dataFormatter'; -import ImageField from '../../components/ImageField'; - -const EditCoursesPage = () => { - const router = useRouter(); - const dispatch = useAppDispatch(); - const initVals = { - title: '', - - description: '', - - instructors: [], - - students: [], - - discussion_boards: [], - }; - const [initialValues, setInitialValues] = useState(initVals); - - const { courses } = useAppSelector((state) => state.courses); - - const { id } = router.query; - - useEffect(() => { - dispatch(fetch({ id: id })); - }, [id]); - - useEffect(() => { - if (typeof courses === 'object') { - setInitialValues(courses); - } - }, [courses]); - - useEffect(() => { - if (typeof courses === 'object') { - const newInitialVal = { ...initVals }; - Object.keys(initVals).forEach((el) => (newInitialVal[el] = courses[el])); - setInitialValues(newInitialVal); - } - }, [courses]); - - const handleSubmit = async (data) => { - await dispatch(update({ id: id, data })); - await router.push('/courses/courses-list'); - }; - - return ( - <> - - {getPageTitle('Edit courses')} - - - - {''} - - - handleSubmit(values)} - > -
    - - - - - - - - - - - - - - - - - - - - - - - - - router.push('/courses/courses-list')} - /> - - -
    -
    -
    - - ); -}; - -EditCoursesPage.getLayout = function getLayout(page: ReactElement) { - return ( - - {page} - - ); -}; - -export default EditCoursesPage; diff --git a/frontend/src/pages/courses/courses-list.tsx b/frontend/src/pages/courses/courses-list.tsx deleted file mode 100644 index a57a9d7..0000000 --- a/frontend/src/pages/courses/courses-list.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { mdiChartTimelineVariant } from '@mdi/js'; -import Head from 'next/head'; -import { uniqueId } from 'lodash'; -import React, { ReactElement, useState } from 'react'; -import CardBox from '../../components/CardBox'; -import LayoutAuthenticated from '../../layouts/Authenticated'; -import SectionMain from '../../components/SectionMain'; -import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; -import { getPageTitle } from '../../config'; -import TableCourses from '../../components/Courses/TableCourses'; -import BaseButton from '../../components/BaseButton'; -import axios from 'axios'; -import Link from 'next/link'; -import { useAppDispatch, useAppSelector } from '../../stores/hooks'; -import CardBoxModal from '../../components/CardBoxModal'; -import DragDropFilePicker from '../../components/DragDropFilePicker'; -import { setRefetch, uploadCsv } from '../../stores/courses/coursesSlice'; - -import { hasPermission } from '../../helpers/userPermissions'; - -const CoursesTablesPage = () => { - const [filterItems, setFilterItems] = useState([]); - const [csvFile, setCsvFile] = useState(null); - const [isModalActive, setIsModalActive] = useState(false); - const [showTableView, setShowTableView] = useState(false); - - const { currentUser } = useAppSelector((state) => state.auth); - - const dispatch = useAppDispatch(); - - const [filters] = useState([ - { label: 'Title', title: 'title' }, - { label: 'Description', title: 'description' }, - - { label: 'Instructors', title: 'instructors' }, - { label: 'Students', title: 'students' }, - { label: 'DiscussionBoards', title: 'discussion_boards' }, - ]); - - const hasCreatePermission = - currentUser && hasPermission(currentUser, 'CREATE_COURSES'); - - const addFilter = () => { - const newItem = { - id: uniqueId(), - fields: { - filterValue: '', - filterValueFrom: '', - filterValueTo: '', - selectedField: '', - }, - }; - newItem.fields.selectedField = filters[0].title; - setFilterItems([...filterItems, newItem]); - }; - - const getCoursesCSV = async () => { - const response = await axios({ - url: '/courses?filetype=csv', - method: 'GET', - responseType: 'blob', - }); - const type = response.headers['content-type']; - const blob = new Blob([response.data], { type: type }); - const link = document.createElement('a'); - link.href = window.URL.createObjectURL(blob); - link.download = 'coursesCSV.csv'; - link.click(); - }; - - const onModalConfirm = async () => { - if (!csvFile) return; - await dispatch(uploadCsv(csvFile)); - dispatch(setRefetch(true)); - setCsvFile(null); - setIsModalActive(false); - }; - - const onModalCancel = () => { - setCsvFile(null); - setIsModalActive(false); - }; - - return ( - <> - - {getPageTitle('Courses')} - - - - {''} - - - {hasCreatePermission && ( - - )} - - - - - {hasCreatePermission && ( - setIsModalActive(true)} - /> - )} - -
    -
    -
    - -
    - Switch to Table -
    -
    - - - - -
    - - - - - ); -}; - -CoursesTablesPage.getLayout = function getLayout(page: ReactElement) { - return ( - - {page} - - ); -}; - -export default CoursesTablesPage; diff --git a/frontend/src/pages/courses/courses-new.tsx b/frontend/src/pages/courses/courses-new.tsx deleted file mode 100644 index 367e272..0000000 --- a/frontend/src/pages/courses/courses-new.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { - mdiAccount, - mdiChartTimelineVariant, - mdiMail, - mdiUpload, -} from '@mdi/js'; -import Head from 'next/head'; -import React, { ReactElement } from 'react'; -import CardBox from '../../components/CardBox'; -import LayoutAuthenticated from '../../layouts/Authenticated'; -import SectionMain from '../../components/SectionMain'; -import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; -import { getPageTitle } from '../../config'; - -import { Field, Form, Formik } from 'formik'; -import FormField from '../../components/FormField'; -import BaseDivider from '../../components/BaseDivider'; -import BaseButtons from '../../components/BaseButtons'; -import BaseButton from '../../components/BaseButton'; -import FormCheckRadio from '../../components/FormCheckRadio'; -import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; -import FormFilePicker from '../../components/FormFilePicker'; -import FormImagePicker from '../../components/FormImagePicker'; -import { SwitchField } from '../../components/SwitchField'; - -import { SelectField } from '../../components/SelectField'; -import { SelectFieldMany } from '../../components/SelectFieldMany'; -import { RichTextField } from '../../components/RichTextField'; - -import { create } from '../../stores/courses/coursesSlice'; -import { useAppDispatch } from '../../stores/hooks'; -import { useRouter } from 'next/router'; -import moment from 'moment'; - -const initialValues = { - title: '', - - description: '', - - instructors: [], - - students: [], - - discussion_boards: [], -}; - -const CoursesNew = () => { - const router = useRouter(); - const dispatch = useAppDispatch(); - - const handleSubmit = async (data) => { - await dispatch(create(data)); - await router.push('/courses/courses-list'); - }; - return ( - <> - - {getPageTitle('New Item')} - - - - {''} - - - handleSubmit(values)} - > -
    - - - - - - - - - - - - - - - - - - - - - - - - - router.push('/courses/courses-list')} - /> - - -
    -
    -
    - - ); -}; - -CoursesNew.getLayout = function getLayout(page: ReactElement) { - return ( - - {page} - - ); -}; - -export default CoursesNew; diff --git a/frontend/src/pages/courses/courses-table.tsx b/frontend/src/pages/courses/courses-table.tsx deleted file mode 100644 index 5f62cb8..0000000 --- a/frontend/src/pages/courses/courses-table.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import { mdiChartTimelineVariant } from '@mdi/js'; -import Head from 'next/head'; -import { uniqueId } from 'lodash'; -import React, { ReactElement, useState } from 'react'; -import CardBox from '../../components/CardBox'; -import LayoutAuthenticated from '../../layouts/Authenticated'; -import SectionMain from '../../components/SectionMain'; -import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; -import { getPageTitle } from '../../config'; -import TableCourses from '../../components/Courses/TableCourses'; -import BaseButton from '../../components/BaseButton'; -import axios from 'axios'; -import Link from 'next/link'; -import { useAppDispatch, useAppSelector } from '../../stores/hooks'; -import CardBoxModal from '../../components/CardBoxModal'; -import DragDropFilePicker from '../../components/DragDropFilePicker'; -import { setRefetch, uploadCsv } from '../../stores/courses/coursesSlice'; - -import { hasPermission } from '../../helpers/userPermissions'; - -const CoursesTablesPage = () => { - const [filterItems, setFilterItems] = useState([]); - const [csvFile, setCsvFile] = useState(null); - const [isModalActive, setIsModalActive] = useState(false); - const [showTableView, setShowTableView] = useState(false); - - const { currentUser } = useAppSelector((state) => state.auth); - - const dispatch = useAppDispatch(); - - const [filters] = useState([ - { label: 'Title', title: 'title' }, - { label: 'Description', title: 'description' }, - - { label: 'Instructors', title: 'instructors' }, - { label: 'Students', title: 'students' }, - { label: 'DiscussionBoards', title: 'discussion_boards' }, - ]); - - const hasCreatePermission = - currentUser && hasPermission(currentUser, 'CREATE_COURSES'); - - const addFilter = () => { - const newItem = { - id: uniqueId(), - fields: { - filterValue: '', - filterValueFrom: '', - filterValueTo: '', - selectedField: '', - }, - }; - newItem.fields.selectedField = filters[0].title; - setFilterItems([...filterItems, newItem]); - }; - - const getCoursesCSV = async () => { - const response = await axios({ - url: '/courses?filetype=csv', - method: 'GET', - responseType: 'blob', - }); - const type = response.headers['content-type']; - const blob = new Blob([response.data], { type: type }); - const link = document.createElement('a'); - link.href = window.URL.createObjectURL(blob); - link.download = 'coursesCSV.csv'; - link.click(); - }; - - const onModalConfirm = async () => { - if (!csvFile) return; - await dispatch(uploadCsv(csvFile)); - dispatch(setRefetch(true)); - setCsvFile(null); - setIsModalActive(false); - }; - - const onModalCancel = () => { - setCsvFile(null); - setIsModalActive(false); - }; - - return ( - <> - - {getPageTitle('Courses')} - - - - {''} - - - {hasCreatePermission && ( - - )} - - - - - {hasCreatePermission && ( - setIsModalActive(true)} - /> - )} - -
    -
    - - - Back to list - -
    -
    - - - -
    - - - - - ); -}; - -CoursesTablesPage.getLayout = function getLayout(page: ReactElement) { - return ( - - {page} - - ); -}; - -export default CoursesTablesPage; diff --git a/frontend/src/pages/courses/courses-view.tsx b/frontend/src/pages/courses/courses-view.tsx deleted file mode 100644 index ecd807a..0000000 --- a/frontend/src/pages/courses/courses-view.tsx +++ /dev/null @@ -1,403 +0,0 @@ -import React, { ReactElement, useEffect } from 'react'; -import Head from 'next/head'; -import DatePicker from 'react-datepicker'; -import 'react-datepicker/dist/react-datepicker.css'; -import dayjs from 'dayjs'; -import { useAppDispatch, useAppSelector } from '../../stores/hooks'; -import { useRouter } from 'next/router'; -import { fetch } from '../../stores/courses/coursesSlice'; -import { saveFile } from '../../helpers/fileSaver'; -import dataFormatter from '../../helpers/dataFormatter'; -import ImageField from '../../components/ImageField'; -import LayoutAuthenticated from '../../layouts/Authenticated'; -import { getPageTitle } from '../../config'; -import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; -import SectionMain from '../../components/SectionMain'; -import CardBox from '../../components/CardBox'; -import BaseButton from '../../components/BaseButton'; -import BaseDivider from '../../components/BaseDivider'; -import { mdiChartTimelineVariant } from '@mdi/js'; -import { SwitchField } from '../../components/SwitchField'; -import FormField from '../../components/FormField'; - -const CoursesView = () => { - const router = useRouter(); - const dispatch = useAppDispatch(); - const { courses } = useAppSelector((state) => state.courses); - - const { id } = router.query; - - function removeLastCharacter(str) { - console.log(str, `str`); - return str.slice(0, -1); - } - - useEffect(() => { - dispatch(fetch({ id })); - }, [dispatch, id]); - - return ( - <> - - {getPageTitle('View courses')} - - - - - - -
    -

    Title

    -

    {courses?.title}

    -
    - -
    -

    Description

    - {courses.description ? ( -

    - ) : ( -

    No data

    - )} -
    - - <> -

    Instructors

    - -
    - - - - - - - - - - - - - - - - - - {courses.instructors && - Array.isArray(courses.instructors) && - courses.instructors.map((item: any) => ( - - router.push(`/users/users-view/?id=${item.id}`) - } - > - - - - - - - - - - - - - ))} - -
    First NameLast NamePhone NumberE-MailDisabledQualifications
    {item.firstName}{item.lastName}{item.phoneNumber}{item.email} - {dataFormatter.booleanFormatter(item.disabled)} - - {item.qualifications} -
    -
    - {!courses?.instructors?.length && ( -
    No data
    - )} -
    - - - <> -

    Students

    - -
    - - - - - - - - - - - - - - - - {courses.students && - Array.isArray(courses.students) && - courses.students.map((item: any) => ( - - router.push(`/users/users-view/?id=${item.id}`) - } - > - - - - - - - - - - - ))} - -
    First NameLast NamePhone NumberE-MailDisabled
    {item.firstName}{item.lastName}{item.phoneNumber}{item.email} - {dataFormatter.booleanFormatter(item.disabled)} -
    -
    - {!courses?.students?.length && ( -
    No data
    - )} -
    - - - <> -

    DiscussionBoards

    - -
    - - - - - - - - {courses.discussion_boards && - Array.isArray(courses.discussion_boards) && - courses.discussion_boards.map((item: any) => ( - - router.push( - `/discussion_boards/discussion_boards-view/?id=${item.id}`, - ) - } - > - - - ))} - -
    Topic
    {item.topic}
    -
    - {!courses?.discussion_boards?.length && ( -
    No data
    - )} -
    - - - <> -

    Analytics Course

    - -
    - - - - - - - - - - - - {courses.analytics_course && - Array.isArray(courses.analytics_course) && - courses.analytics_course.map((item: any) => ( - - router.push( - `/analytics/analytics-view/?id=${item.id}`, - ) - } - > - - - - - - - ))} - -
    StudentEngagementCompletionRateInstructorPerformance
    - {item.student_engagement} - - {item.completion_rate} - - {item.instructor_performance} -
    -
    - {!courses?.analytics_course?.length && ( -
    No data
    - )} -
    - - - <> -

    Discussion_boards Course

    - -
    - - - - - - - - {courses.discussion_boards_course && - Array.isArray(courses.discussion_boards_course) && - courses.discussion_boards_course.map((item: any) => ( - - router.push( - `/discussion_boards/discussion_boards-view/?id=${item.id}`, - ) - } - > - - - ))} - -
    Topic
    {item.topic}
    -
    - {!courses?.discussion_boards_course?.length && ( -
    No data
    - )} -
    - - - <> -

    Enrollments Course

    - -
    - - - - - - - - {courses.enrollments_course && - Array.isArray(courses.enrollments_course) && - courses.enrollments_course.map((item: any) => ( - - router.push( - `/enrollments/enrollments-view/?id=${item.id}`, - ) - } - > - - - ))} - -
    PaymentStatus
    - {item.payment_status} -
    -
    - {!courses?.enrollments_course?.length && ( -
    No data
    - )} -
    - - - <> -

    Grades Course

    - -
    - - - - - - - - {courses.grades_course && - Array.isArray(courses.grades_course) && - courses.grades_course.map((item: any) => ( - - router.push(`/grades/grades-view/?id=${item.id}`) - } - > - - - ))} - -
    Grade
    {item.grade}
    -
    - {!courses?.grades_course?.length && ( -
    No data
    - )} -
    - - - - - router.push('/courses/courses-list')} - /> -
    -
    - - ); -}; - -CoursesView.getLayout = function getLayout(page: ReactElement) { - return ( - - {page} - - ); -}; - -export default CoursesView; diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index fc83c6a..0088c8e 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -29,17 +29,10 @@ const Dashboard = () => { }); const [users, setUsers] = React.useState(loadingMessage); - const [analytics, setAnalytics] = React.useState(loadingMessage); - const [courses, setCourses] = React.useState(loadingMessage); - const [discussion_boards, setDiscussion_boards] = - React.useState(loadingMessage); - const [enrollments, setEnrollments] = React.useState(loadingMessage); - const [grades, setGrades] = React.useState(loadingMessage); - const [instructors, setInstructors] = React.useState(loadingMessage); - const [posts, setPosts] = React.useState(loadingMessage); - const [students, setStudents] = React.useState(loadingMessage); const [roles, setRoles] = React.useState(loadingMessage); const [permissions, setPermissions] = React.useState(loadingMessage); + const [mcs_pyq, setMcs_pyq] = React.useState(loadingMessage); + const [course, setCourse] = React.useState(loadingMessage); const [widgetsRole, setWidgetsRole] = React.useState({ role: { value: '', label: '' }, @@ -50,32 +43,8 @@ const Dashboard = () => { const { rolesWidgets, loading } = useAppSelector((state) => state.roles); async function loadData() { - const entities = [ - 'users', - 'analytics', - 'courses', - 'discussion_boards', - 'enrollments', - 'grades', - 'instructors', - 'posts', - 'students', - 'roles', - 'permissions', - ]; - const fns = [ - setUsers, - setAnalytics, - setCourses, - setDiscussion_boards, - setEnrollments, - setGrades, - setInstructors, - setPosts, - setStudents, - setRoles, - setPermissions, - ]; + const entities = ['users', 'roles', 'permissions', 'mcs_pyq', 'course']; + const fns = [setUsers, setRoles, setPermissions, setMcs_pyq, setCourse]; const requests = entities.map((entity, index) => { if (hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) { @@ -221,296 +190,6 @@ const Dashboard = () => { )} - {hasPermission(currentUser, 'READ_ANALYTICS') && ( - -
    -
    -
    -
    - Analytics -
    -
    - {analytics} -
    -
    -
    - -
    -
    -
    - - )} - - {hasPermission(currentUser, 'READ_COURSES') && ( - -
    -
    -
    -
    - Courses -
    -
    - {courses} -
    -
    -
    - -
    -
    -
    - - )} - - {hasPermission(currentUser, 'READ_DISCUSSION_BOARDS') && ( - -
    -
    -
    -
    - Discussion boards -
    -
    - {discussion_boards} -
    -
    -
    - -
    -
    -
    - - )} - - {hasPermission(currentUser, 'READ_ENROLLMENTS') && ( - -
    -
    -
    -
    - Enrollments -
    -
    - {enrollments} -
    -
    -
    - -
    -
    -
    - - )} - - {hasPermission(currentUser, 'READ_GRADES') && ( - -
    -
    -
    -
    - Grades -
    -
    - {grades} -
    -
    -
    - -
    -
    -
    - - )} - - {hasPermission(currentUser, 'READ_INSTRUCTORS') && ( - -
    -
    -
    -
    - Instructors -
    -
    - {instructors} -
    -
    -
    - -
    -
    -
    - - )} - - {hasPermission(currentUser, 'READ_POSTS') && ( - -
    -
    -
    -
    - Posts -
    -
    - {posts} -
    -
    -
    - -
    -
    -
    - - )} - - {hasPermission(currentUser, 'READ_STUDENTS') && ( - -
    -
    -
    -
    - Students -
    -
    - {students} -
    -
    -
    - -
    -
    -
    - - )} - {hasPermission(currentUser, 'READ_ROLES') && (
    {
    )} + + {hasPermission(currentUser, 'READ_MCS_PYQ') && ( + +
    +
    +
    +
    + Mcs pyq +
    +
    + {mcs_pyq} +
    +
    +
    + +
    +
    +
    + + )} + + {hasPermission(currentUser, 'READ_COURSE') && ( + +
    +
    +
    +
    + Course +
    +
    + {course} +
    +
    +
    + +
    +
    +
    + + )}
    diff --git a/frontend/src/pages/discussion_boards/discussion_boards-list.tsx b/frontend/src/pages/discussion_boards/discussion_boards-list.tsx deleted file mode 100644 index 0e221c5..0000000 --- a/frontend/src/pages/discussion_boards/discussion_boards-list.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { mdiChartTimelineVariant } from '@mdi/js'; -import Head from 'next/head'; -import { uniqueId } from 'lodash'; -import React, { ReactElement, useState } from 'react'; -import CardBox from '../../components/CardBox'; -import LayoutAuthenticated from '../../layouts/Authenticated'; -import SectionMain from '../../components/SectionMain'; -import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; -import { getPageTitle } from '../../config'; -import TableDiscussion_boards from '../../components/Discussion_boards/TableDiscussion_boards'; -import BaseButton from '../../components/BaseButton'; -import axios from 'axios'; -import Link from 'next/link'; -import { useAppDispatch, useAppSelector } from '../../stores/hooks'; -import CardBoxModal from '../../components/CardBoxModal'; -import DragDropFilePicker from '../../components/DragDropFilePicker'; -import { - setRefetch, - uploadCsv, -} from '../../stores/discussion_boards/discussion_boardsSlice'; - -import { hasPermission } from '../../helpers/userPermissions'; - -const Discussion_boardsTablesPage = () => { - const [filterItems, setFilterItems] = useState([]); - const [csvFile, setCsvFile] = useState(null); - const [isModalActive, setIsModalActive] = useState(false); - const [showTableView, setShowTableView] = useState(false); - - const { currentUser } = useAppSelector((state) => state.auth); - - const dispatch = useAppDispatch(); - - const [filters] = useState([ - { label: 'Topic', title: 'topic' }, - - { label: 'Course', title: 'course' }, - - { label: 'Posts', title: 'posts' }, - ]); - - const hasCreatePermission = - currentUser && hasPermission(currentUser, 'CREATE_DISCUSSION_BOARDS'); - - const addFilter = () => { - const newItem = { - id: uniqueId(), - fields: { - filterValue: '', - filterValueFrom: '', - filterValueTo: '', - selectedField: '', - }, - }; - newItem.fields.selectedField = filters[0].title; - setFilterItems([...filterItems, newItem]); - }; - - const getDiscussion_boardsCSV = async () => { - const response = await axios({ - url: '/discussion_boards?filetype=csv', - method: 'GET', - responseType: 'blob', - }); - const type = response.headers['content-type']; - const blob = new Blob([response.data], { type: type }); - const link = document.createElement('a'); - link.href = window.URL.createObjectURL(blob); - link.download = 'discussion_boardsCSV.csv'; - link.click(); - }; - - const onModalConfirm = async () => { - if (!csvFile) return; - await dispatch(uploadCsv(csvFile)); - dispatch(setRefetch(true)); - setCsvFile(null); - setIsModalActive(false); - }; - - const onModalCancel = () => { - setCsvFile(null); - setIsModalActive(false); - }; - - return ( - <> - - {getPageTitle('Discussion_boards')} - - - - {''} - - - {hasCreatePermission && ( - - )} - - - - - {hasCreatePermission && ( - setIsModalActive(true)} - /> - )} - -
    -
    -
    - -
    - - Switch to Table - -
    -
    - - - - -
    - - - - - ); -}; - -Discussion_boardsTablesPage.getLayout = function getLayout(page: ReactElement) { - return ( - - {page} - - ); -}; - -export default Discussion_boardsTablesPage; diff --git a/frontend/src/pages/discussion_boards/discussion_boards-table.tsx b/frontend/src/pages/discussion_boards/discussion_boards-table.tsx deleted file mode 100644 index db872ee..0000000 --- a/frontend/src/pages/discussion_boards/discussion_boards-table.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import { mdiChartTimelineVariant } from '@mdi/js'; -import Head from 'next/head'; -import { uniqueId } from 'lodash'; -import React, { ReactElement, useState } from 'react'; -import CardBox from '../../components/CardBox'; -import LayoutAuthenticated from '../../layouts/Authenticated'; -import SectionMain from '../../components/SectionMain'; -import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; -import { getPageTitle } from '../../config'; -import TableDiscussion_boards from '../../components/Discussion_boards/TableDiscussion_boards'; -import BaseButton from '../../components/BaseButton'; -import axios from 'axios'; -import Link from 'next/link'; -import { useAppDispatch, useAppSelector } from '../../stores/hooks'; -import CardBoxModal from '../../components/CardBoxModal'; -import DragDropFilePicker from '../../components/DragDropFilePicker'; -import { - setRefetch, - uploadCsv, -} from '../../stores/discussion_boards/discussion_boardsSlice'; - -import { hasPermission } from '../../helpers/userPermissions'; - -const Discussion_boardsTablesPage = () => { - const [filterItems, setFilterItems] = useState([]); - const [csvFile, setCsvFile] = useState(null); - const [isModalActive, setIsModalActive] = useState(false); - const [showTableView, setShowTableView] = useState(false); - - const { currentUser } = useAppSelector((state) => state.auth); - - const dispatch = useAppDispatch(); - - const [filters] = useState([ - { label: 'Topic', title: 'topic' }, - - { label: 'Course', title: 'course' }, - - { label: 'Posts', title: 'posts' }, - ]); - - const hasCreatePermission = - currentUser && hasPermission(currentUser, 'CREATE_DISCUSSION_BOARDS'); - - const addFilter = () => { - const newItem = { - id: uniqueId(), - fields: { - filterValue: '', - filterValueFrom: '', - filterValueTo: '', - selectedField: '', - }, - }; - newItem.fields.selectedField = filters[0].title; - setFilterItems([...filterItems, newItem]); - }; - - const getDiscussion_boardsCSV = async () => { - const response = await axios({ - url: '/discussion_boards?filetype=csv', - method: 'GET', - responseType: 'blob', - }); - const type = response.headers['content-type']; - const blob = new Blob([response.data], { type: type }); - const link = document.createElement('a'); - link.href = window.URL.createObjectURL(blob); - link.download = 'discussion_boardsCSV.csv'; - link.click(); - }; - - const onModalConfirm = async () => { - if (!csvFile) return; - await dispatch(uploadCsv(csvFile)); - dispatch(setRefetch(true)); - setCsvFile(null); - setIsModalActive(false); - }; - - const onModalCancel = () => { - setCsvFile(null); - setIsModalActive(false); - }; - - return ( - <> - - {getPageTitle('Discussion_boards')} - - - - {''} - - - {hasCreatePermission && ( - - )} - - - - - {hasCreatePermission && ( - setIsModalActive(true)} - /> - )} - -
    -
    - - - Back to list - -
    -
    - - - -
    - - - - - ); -}; - -Discussion_boardsTablesPage.getLayout = function getLayout(page: ReactElement) { - return ( - - {page} - - ); -}; - -export default Discussion_boardsTablesPage; diff --git a/frontend/src/pages/discussion_boards/discussion_boards-view.tsx b/frontend/src/pages/discussion_boards/discussion_boards-view.tsx deleted file mode 100644 index dd49eea..0000000 --- a/frontend/src/pages/discussion_boards/discussion_boards-view.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import React, { ReactElement, useEffect } from 'react'; -import Head from 'next/head'; -import DatePicker from 'react-datepicker'; -import 'react-datepicker/dist/react-datepicker.css'; -import dayjs from 'dayjs'; -import { useAppDispatch, useAppSelector } from '../../stores/hooks'; -import { useRouter } from 'next/router'; -import { fetch } from '../../stores/discussion_boards/discussion_boardsSlice'; -import { saveFile } from '../../helpers/fileSaver'; -import dataFormatter from '../../helpers/dataFormatter'; -import ImageField from '../../components/ImageField'; -import LayoutAuthenticated from '../../layouts/Authenticated'; -import { getPageTitle } from '../../config'; -import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; -import SectionMain from '../../components/SectionMain'; -import CardBox from '../../components/CardBox'; -import BaseButton from '../../components/BaseButton'; -import BaseDivider from '../../components/BaseDivider'; -import { mdiChartTimelineVariant } from '@mdi/js'; -import { SwitchField } from '../../components/SwitchField'; -import FormField from '../../components/FormField'; - -const Discussion_boardsView = () => { - const router = useRouter(); - const dispatch = useAppDispatch(); - const { discussion_boards } = useAppSelector( - (state) => state.discussion_boards, - ); - - const { id } = router.query; - - function removeLastCharacter(str) { - console.log(str, `str`); - return str.slice(0, -1); - } - - useEffect(() => { - dispatch(fetch({ id })); - }, [dispatch, id]); - - return ( - <> - - {getPageTitle('View discussion_boards')} - - - - - - -
    -

    Course

    - -

    {discussion_boards?.course?.title ?? 'No data'}

    -
    - -
    -

    Topic

    -

    {discussion_boards?.topic}

    -
    - - <> -

    Posts

    - -
    - - - - - - - - {discussion_boards.posts && - Array.isArray(discussion_boards.posts) && - discussion_boards.posts.map((item: any) => ( - - router.push(`/posts/posts-view/?id=${item.id}`) - } - > - - - ))} - -
    PostedAt
    - {dataFormatter.dateTimeFormatter(item.posted_at)} -
    -
    - {!discussion_boards?.posts?.length && ( -
    No data
    - )} -
    - - - <> -

    Posts DiscussionBoard

    - -
    - - - - - - - - {discussion_boards.posts_discussion_board && - Array.isArray(discussion_boards.posts_discussion_board) && - discussion_boards.posts_discussion_board.map( - (item: any) => ( - - router.push(`/posts/posts-view/?id=${item.id}`) - } - > - - - ), - )} - -
    PostedAt
    - {dataFormatter.dateTimeFormatter(item.posted_at)} -
    -
    - {!discussion_boards?.posts_discussion_board?.length && ( -
    No data
    - )} -
    - - - - - - router.push('/discussion_boards/discussion_boards-list') - } - /> -
    -
    - - ); -}; - -Discussion_boardsView.getLayout = function getLayout(page: ReactElement) { - return ( - - {page} - - ); -}; - -export default Discussion_boardsView; diff --git a/frontend/src/pages/enrollments/[enrollmentsId].tsx b/frontend/src/pages/enrollments/[enrollmentsId].tsx deleted file mode 100644 index e58ba47..0000000 --- a/frontend/src/pages/enrollments/[enrollmentsId].tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; -import Head from 'next/head'; -import React, { ReactElement, useEffect, useState } from 'react'; -import DatePicker from 'react-datepicker'; -import 'react-datepicker/dist/react-datepicker.css'; -import dayjs from 'dayjs'; - -import CardBox from '../../components/CardBox'; -import LayoutAuthenticated from '../../layouts/Authenticated'; -import SectionMain from '../../components/SectionMain'; -import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; -import { getPageTitle } from '../../config'; - -import { Field, Form, Formik } from 'formik'; -import FormField from '../../components/FormField'; -import BaseDivider from '../../components/BaseDivider'; -import BaseButtons from '../../components/BaseButtons'; -import BaseButton from '../../components/BaseButton'; -import FormCheckRadio from '../../components/FormCheckRadio'; -import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; -import FormFilePicker from '../../components/FormFilePicker'; -import FormImagePicker from '../../components/FormImagePicker'; -import { SelectField } from '../../components/SelectField'; -import { SelectFieldMany } from '../../components/SelectFieldMany'; -import { SwitchField } from '../../components/SwitchField'; -import { RichTextField } from '../../components/RichTextField'; - -import { update, fetch } from '../../stores/enrollments/enrollmentsSlice'; -import { useAppDispatch, useAppSelector } from '../../stores/hooks'; -import { useRouter } from 'next/router'; -import { saveFile } from '../../helpers/fileSaver'; -import dataFormatter from '../../helpers/dataFormatter'; -import ImageField from '../../components/ImageField'; - -const EditEnrollments = () => { - const router = useRouter(); - const dispatch = useAppDispatch(); - const initVals = { - student: null, - - course: null, - - payment_status: '', - }; - const [initialValues, setInitialValues] = useState(initVals); - - const { enrollments } = useAppSelector((state) => state.enrollments); - - const { enrollmentsId } = router.query; - - useEffect(() => { - dispatch(fetch({ id: enrollmentsId })); - }, [enrollmentsId]); - - useEffect(() => { - if (typeof enrollments === 'object') { - setInitialValues(enrollments); - } - }, [enrollments]); - - useEffect(() => { - if (typeof enrollments === 'object') { - const newInitialVal = { ...initVals }; - - Object.keys(initVals).forEach( - (el) => (newInitialVal[el] = enrollments[el]), - ); - - setInitialValues(newInitialVal); - } - }, [enrollments]); - - const handleSubmit = async (data) => { - await dispatch(update({ id: enrollmentsId, data })); - await router.push('/enrollments/enrollments-list'); - }; - - return ( - <> - - {getPageTitle('Edit enrollments')} - - - - {''} - - - handleSubmit(values)} - > -
    - - - - - - - - - - - - - - - - - - - - - router.push('/enrollments/enrollments-list')} - /> - - -
    -
    -
    - - ); -}; - -EditEnrollments.getLayout = function getLayout(page: ReactElement) { - return ( - - {page} - - ); -}; - -export default EditEnrollments; diff --git a/frontend/src/pages/enrollments/enrollments-edit.tsx b/frontend/src/pages/enrollments/enrollments-edit.tsx deleted file mode 100644 index ed0bf96..0000000 --- a/frontend/src/pages/enrollments/enrollments-edit.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { mdiChartTimelineVariant, mdiUpload } from '@mdi/js'; -import Head from 'next/head'; -import React, { ReactElement, useEffect, useState } from 'react'; -import DatePicker from 'react-datepicker'; -import 'react-datepicker/dist/react-datepicker.css'; -import dayjs from 'dayjs'; - -import CardBox from '../../components/CardBox'; -import LayoutAuthenticated from '../../layouts/Authenticated'; -import SectionMain from '../../components/SectionMain'; -import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; -import { getPageTitle } from '../../config'; - -import { Field, Form, Formik } from 'formik'; -import FormField from '../../components/FormField'; -import BaseDivider from '../../components/BaseDivider'; -import BaseButtons from '../../components/BaseButtons'; -import BaseButton from '../../components/BaseButton'; -import FormCheckRadio from '../../components/FormCheckRadio'; -import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; -import FormFilePicker from '../../components/FormFilePicker'; -import FormImagePicker from '../../components/FormImagePicker'; -import { SelectField } from '../../components/SelectField'; -import { SelectFieldMany } from '../../components/SelectFieldMany'; -import { SwitchField } from '../../components/SwitchField'; -import { RichTextField } from '../../components/RichTextField'; - -import { update, fetch } from '../../stores/enrollments/enrollmentsSlice'; -import { useAppDispatch, useAppSelector } from '../../stores/hooks'; -import { useRouter } from 'next/router'; -import { saveFile } from '../../helpers/fileSaver'; -import dataFormatter from '../../helpers/dataFormatter'; -import ImageField from '../../components/ImageField'; - -const EditEnrollmentsPage = () => { - const router = useRouter(); - const dispatch = useAppDispatch(); - const initVals = { - student: null, - - course: null, - - payment_status: '', - }; - const [initialValues, setInitialValues] = useState(initVals); - - const { enrollments } = useAppSelector((state) => state.enrollments); - - const { id } = router.query; - - useEffect(() => { - dispatch(fetch({ id: id })); - }, [id]); - - useEffect(() => { - if (typeof enrollments === 'object') { - setInitialValues(enrollments); - } - }, [enrollments]); - - useEffect(() => { - if (typeof enrollments === 'object') { - const newInitialVal = { ...initVals }; - Object.keys(initVals).forEach( - (el) => (newInitialVal[el] = enrollments[el]), - ); - setInitialValues(newInitialVal); - } - }, [enrollments]); - - const handleSubmit = async (data) => { - await dispatch(update({ id: id, data })); - await router.push('/enrollments/enrollments-list'); - }; - - return ( - <> - - {getPageTitle('Edit enrollments')} - - - - {''} - - - handleSubmit(values)} - > -
    - - - - - - - - - - - - - - - - - - - - - router.push('/enrollments/enrollments-list')} - /> - - -
    -
    -
    - - ); -}; - -EditEnrollmentsPage.getLayout = function getLayout(page: ReactElement) { - return ( - - {page} - - ); -}; - -export default EditEnrollmentsPage; diff --git a/frontend/src/pages/enrollments/enrollments-new.tsx b/frontend/src/pages/enrollments/enrollments-new.tsx deleted file mode 100644 index fd603b2..0000000 --- a/frontend/src/pages/enrollments/enrollments-new.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { - mdiAccount, - mdiChartTimelineVariant, - mdiMail, - mdiUpload, -} from '@mdi/js'; -import Head from 'next/head'; -import React, { ReactElement } from 'react'; -import CardBox from '../../components/CardBox'; -import LayoutAuthenticated from '../../layouts/Authenticated'; -import SectionMain from '../../components/SectionMain'; -import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; -import { getPageTitle } from '../../config'; - -import { Field, Form, Formik } from 'formik'; -import FormField from '../../components/FormField'; -import BaseDivider from '../../components/BaseDivider'; -import BaseButtons from '../../components/BaseButtons'; -import BaseButton from '../../components/BaseButton'; -import FormCheckRadio from '../../components/FormCheckRadio'; -import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; -import FormFilePicker from '../../components/FormFilePicker'; -import FormImagePicker from '../../components/FormImagePicker'; -import { SwitchField } from '../../components/SwitchField'; - -import { SelectField } from '../../components/SelectField'; -import { SelectFieldMany } from '../../components/SelectFieldMany'; -import { RichTextField } from '../../components/RichTextField'; - -import { create } from '../../stores/enrollments/enrollmentsSlice'; -import { useAppDispatch } from '../../stores/hooks'; -import { useRouter } from 'next/router'; -import moment from 'moment'; - -const initialValues = { - student: '', - - course: '', - - payment_status: 'pending', -}; - -const EnrollmentsNew = () => { - const router = useRouter(); - const dispatch = useAppDispatch(); - - const handleSubmit = async (data) => { - await dispatch(create(data)); - await router.push('/enrollments/enrollments-list'); - }; - return ( - <> - - {getPageTitle('New Item')} - - - - {''} - - - handleSubmit(values)} - > -
    - - - - - - - - - - - - - - - - - - - - - router.push('/enrollments/enrollments-list')} - /> - - -
    -
    -
    - - ); -}; - -EnrollmentsNew.getLayout = function getLayout(page: ReactElement) { - return ( - - {page} - - ); -}; - -export default EnrollmentsNew; diff --git a/frontend/src/pages/enrollments/enrollments-table.tsx b/frontend/src/pages/enrollments/enrollments-table.tsx deleted file mode 100644 index cdcfce4..0000000 --- a/frontend/src/pages/enrollments/enrollments-table.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import { mdiChartTimelineVariant } from '@mdi/js'; -import Head from 'next/head'; -import { uniqueId } from 'lodash'; -import React, { ReactElement, useState } from 'react'; -import CardBox from '../../components/CardBox'; -import LayoutAuthenticated from '../../layouts/Authenticated'; -import SectionMain from '../../components/SectionMain'; -import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; -import { getPageTitle } from '../../config'; -import TableEnrollments from '../../components/Enrollments/TableEnrollments'; -import BaseButton from '../../components/BaseButton'; -import axios from 'axios'; -import Link from 'next/link'; -import { useAppDispatch, useAppSelector } from '../../stores/hooks'; -import CardBoxModal from '../../components/CardBoxModal'; -import DragDropFilePicker from '../../components/DragDropFilePicker'; -import { - setRefetch, - uploadCsv, -} from '../../stores/enrollments/enrollmentsSlice'; - -import { hasPermission } from '../../helpers/userPermissions'; - -const EnrollmentsTablesPage = () => { - const [filterItems, setFilterItems] = useState([]); - const [csvFile, setCsvFile] = useState(null); - const [isModalActive, setIsModalActive] = useState(false); - const [showTableView, setShowTableView] = useState(false); - - const { currentUser } = useAppSelector((state) => state.auth); - - const dispatch = useAppDispatch(); - - const [filters] = useState([ - { label: 'Student', title: 'student' }, - - { label: 'Course', title: 'course' }, - - { - label: 'PaymentStatus', - title: 'payment_status', - type: 'enum', - options: ['pending', 'completed'], - }, - ]); - - const hasCreatePermission = - currentUser && hasPermission(currentUser, 'CREATE_ENROLLMENTS'); - - const addFilter = () => { - const newItem = { - id: uniqueId(), - fields: { - filterValue: '', - filterValueFrom: '', - filterValueTo: '', - selectedField: '', - }, - }; - newItem.fields.selectedField = filters[0].title; - setFilterItems([...filterItems, newItem]); - }; - - const getEnrollmentsCSV = async () => { - const response = await axios({ - url: '/enrollments?filetype=csv', - method: 'GET', - responseType: 'blob', - }); - const type = response.headers['content-type']; - const blob = new Blob([response.data], { type: type }); - const link = document.createElement('a'); - link.href = window.URL.createObjectURL(blob); - link.download = 'enrollmentsCSV.csv'; - link.click(); - }; - - const onModalConfirm = async () => { - if (!csvFile) return; - await dispatch(uploadCsv(csvFile)); - dispatch(setRefetch(true)); - setCsvFile(null); - setIsModalActive(false); - }; - - const onModalCancel = () => { - setCsvFile(null); - setIsModalActive(false); - }; - - return ( - <> - - {getPageTitle('Enrollments')} - - - - {''} - - - {hasCreatePermission && ( - - )} - - - - - {hasCreatePermission && ( - setIsModalActive(true)} - /> - )} - -
    -
    - - - Back to table - -
    -
    - - - -
    - - - - - ); -}; - -EnrollmentsTablesPage.getLayout = function getLayout(page: ReactElement) { - return ( - - {page} - - ); -}; - -export default EnrollmentsTablesPage; diff --git a/frontend/src/pages/instructors/instructors-list.tsx b/frontend/src/pages/instructors/instructors-list.tsx deleted file mode 100644 index ea64db5..0000000 --- a/frontend/src/pages/instructors/instructors-list.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import { mdiChartTimelineVariant } from '@mdi/js'; -import Head from 'next/head'; -import { uniqueId } from 'lodash'; -import React, { ReactElement, useState } from 'react'; -import CardBox from '../../components/CardBox'; -import LayoutAuthenticated from '../../layouts/Authenticated'; -import SectionMain from '../../components/SectionMain'; -import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; -import { getPageTitle } from '../../config'; -import TableInstructors from '../../components/Instructors/TableInstructors'; -import BaseButton from '../../components/BaseButton'; -import axios from 'axios'; -import Link from 'next/link'; -import { useAppDispatch, useAppSelector } from '../../stores/hooks'; -import CardBoxModal from '../../components/CardBoxModal'; -import DragDropFilePicker from '../../components/DragDropFilePicker'; -import { - setRefetch, - uploadCsv, -} from '../../stores/instructors/instructorsSlice'; - -import { hasPermission } from '../../helpers/userPermissions'; - -const InstructorsTablesPage = () => { - const [filterItems, setFilterItems] = useState([]); - const [csvFile, setCsvFile] = useState(null); - const [isModalActive, setIsModalActive] = useState(false); - const [showTableView, setShowTableView] = useState(false); - - const { currentUser } = useAppSelector((state) => state.auth); - - const dispatch = useAppDispatch(); - - const [filters] = useState([ - { label: 'Qualifications', title: 'qualifications' }, - - { label: 'User', title: 'user' }, - - { label: 'Courses', title: 'courses' }, - ]); - - const hasCreatePermission = - currentUser && hasPermission(currentUser, 'CREATE_INSTRUCTORS'); - - const addFilter = () => { - const newItem = { - id: uniqueId(), - fields: { - filterValue: '', - filterValueFrom: '', - filterValueTo: '', - selectedField: '', - }, - }; - newItem.fields.selectedField = filters[0].title; - setFilterItems([...filterItems, newItem]); - }; - - const getInstructorsCSV = async () => { - const response = await axios({ - url: '/instructors?filetype=csv', - method: 'GET', - responseType: 'blob', - }); - const type = response.headers['content-type']; - const blob = new Blob([response.data], { type: type }); - const link = document.createElement('a'); - link.href = window.URL.createObjectURL(blob); - link.download = 'instructorsCSV.csv'; - link.click(); - }; - - const onModalConfirm = async () => { - if (!csvFile) return; - await dispatch(uploadCsv(csvFile)); - dispatch(setRefetch(true)); - setCsvFile(null); - setIsModalActive(false); - }; - - const onModalCancel = () => { - setCsvFile(null); - setIsModalActive(false); - }; - - return ( - <> - - {getPageTitle('Instructors')} - - - - {''} - - - {hasCreatePermission && ( - - )} - - - - - {hasCreatePermission && ( - setIsModalActive(true)} - /> - )} - -
    -
    -
    - -
    - Switch to Table -
    -
    - - - - -
    - - - - - ); -}; - -InstructorsTablesPage.getLayout = function getLayout(page: ReactElement) { - return ( - - {page} - - ); -}; - -export default InstructorsTablesPage; diff --git a/frontend/src/pages/instructors/instructors-view.tsx b/frontend/src/pages/instructors/instructors-view.tsx deleted file mode 100644 index 79f615f..0000000 --- a/frontend/src/pages/instructors/instructors-view.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import React, { ReactElement, useEffect } from 'react'; -import Head from 'next/head'; -import DatePicker from 'react-datepicker'; -import 'react-datepicker/dist/react-datepicker.css'; -import dayjs from 'dayjs'; -import { useAppDispatch, useAppSelector } from '../../stores/hooks'; -import { useRouter } from 'next/router'; -import { fetch } from '../../stores/instructors/instructorsSlice'; -import { saveFile } from '../../helpers/fileSaver'; -import dataFormatter from '../../helpers/dataFormatter'; -import ImageField from '../../components/ImageField'; -import LayoutAuthenticated from '../../layouts/Authenticated'; -import { getPageTitle } from '../../config'; -import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; -import SectionMain from '../../components/SectionMain'; -import CardBox from '../../components/CardBox'; -import BaseButton from '../../components/BaseButton'; -import BaseDivider from '../../components/BaseDivider'; -import { mdiChartTimelineVariant } from '@mdi/js'; -import { SwitchField } from '../../components/SwitchField'; -import FormField from '../../components/FormField'; - -const InstructorsView = () => { - const router = useRouter(); - const dispatch = useAppDispatch(); - const { instructors } = useAppSelector((state) => state.instructors); - - const { id } = router.query; - - function removeLastCharacter(str) { - console.log(str, `str`); - return str.slice(0, -1); - } - - useEffect(() => { - dispatch(fetch({ id })); - }, [dispatch, id]); - - return ( - <> - - {getPageTitle('View instructors')} - - - - - - -
    -

    User

    - -

    {instructors?.user?.firstName ?? 'No data'}

    -
    - - -