+
{{ epa.name }}
-
-
-
-
+
-
-
-
- {{ formatDate(ea.encounterDate.toString()) }} - {{ ea.enteredByName }}
-
- {{ ea.comment }}
-
-
-
+
+
+
+
+
+
+
+ {{ formatDate(a.encounterDate.toString()) }}
+ {{ a.enteredByName }}
+
+
+ {{ a.serviceName }}
+
+
+ {{ a.levelName }}
+
+
+ {{ a.comment }}
+
+
+
+
\ No newline at end of file
diff --git a/VueApp/src/CTS/router/index.ts b/VueApp/src/CTS/router/index.ts
index aea9fb7..89aa0a9 100644
--- a/VueApp/src/CTS/router/index.ts
+++ b/VueApp/src/CTS/router/index.ts
@@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router'
import routes from './routes'
import useRequireLogin from '@/composables/RequireLogin'
+import checkHasOnePermission from '@/composables/CheckPagePermission'
const baseUrl = import.meta.env.VITE_VIPER_HOME
const router = createRouter({
@@ -11,7 +12,13 @@ const router = createRouter({
router.beforeEach(async (to) => {
const { requireLogin } = useRequireLogin(to)
- return requireLogin(true, "SVMSecure.CTS")
+ await requireLogin(true, "SVMSecure.CTS")
+ if (to.meta.permissions != undefined) {
+ const hasPerm = checkHasOnePermission(to.meta.permissions as string[])
+ if (!hasPerm) {
+ return { name: "CtsHome" }
+ }
+ }
})
export default router
\ No newline at end of file
diff --git a/VueApp/src/CTS/router/routes.ts b/VueApp/src/CTS/router/routes.ts
index 929f987..b41a6ba 100644
--- a/VueApp/src/CTS/router/routes.ts
+++ b/VueApp/src/CTS/router/routes.ts
@@ -1,7 +1,6 @@
import ViperLayout from '@/layouts/ViperLayout.vue'
import ViperLayoutSimple from '@/layouts/ViperLayoutSimple.vue'
-const viperURL = import.meta.env.VITE_VIPER_HOME
const ctsBreadcrumbs = [{ url: "Home", name: "Return to CTS 2.0" }]
const routes = [
@@ -20,60 +19,101 @@ const routes = [
{
path: '/CTS/MyAssessments',
name: 'MyAssessments',
- meta: { layout: ViperLayout },
+ meta: { layout: ViperLayout, permissions: ["SVMSecure.CTS.Students"] },
component: () => import('@/CTS/pages/MyAssessments.vue'),
},
/* Assessments */
{
path: '/CTS/EPA',
- meta: { layout: ViperLayoutSimple, breadcrumbs: ctsBreadcrumbs },
+ meta: { layout: ViperLayoutSimple, breadcrumbs: ctsBreadcrumbs, permissions: ["SVMSecure.CTS.Manage", "SVMSecure.CTS.AssessClinical"] },
component: () => import('@/CTS/pages/AssessmentEpa.vue'),
},
{
path: '/CTS/AssessmentList',
name: 'AssessmentList',
- meta: { layout: ViperLayout },
+ meta: { layout: ViperLayout, permissions: ["SVMSecure.CTS.Manage", "SVMSecure.CTS.StudentAssessments", "SVMSecure.CTS.AssessClinical"] },
component: () => import('@/CTS/pages/AssessmentList.vue'),
},
{
path: '/CTS/AssessmentEpaEdit',
name: 'AssessmentEpaEdit',
- meta: { layout: ViperLayoutSimple, breadcrumbs: ctsBreadcrumbs },
+ meta: { layout: ViperLayoutSimple, breadcrumbs: ctsBreadcrumbs, permissions: ["SVMSecure.CTS.Manage", "SVMSecure.CTS.AssessClinical"] },
component: () => import('@/CTS/pages/AssessmentEpaEdit.vue'),
},
+ /* Course Competencies */
+ {
+ path: '/CTS/ManageCourseCompetencies',
+ meta: { layout: ViperLayout, permissions: ["SVMSecure.CTS.Manage"] },
+ component: () => import('@/CTS/pages/ManageCourseCompetencies.vue'),
+ },
+ {
+ path: '/CTS/ManageLegacyCompetencyMapping',
+ meta: { layout: ViperLayout, permissions: ["SVMSecure.CTS.Manage"] },
+ component: () => import('@/CTS/pages/ManageLegacyCompetencyMapping.vue'),
+ },
+ {
+ path: '/CTS/ManageSessionCompetencies',
+ meta: { layout: ViperLayout, permissions: ["SVMSecure.CTS.Manage"] },
+ component: () => import('@/CTS/pages/ManageSessionCompetencies.vue'),
+ },
/* Application Management */
+ {
+ path: '/CTS/ManageCompetencies',
+ meta: { layout: ViperLayout, permissions: ["SVMSecure.CTS.Manage"] },
+ component: () => import('@/CTS/pages/ManageCompetencies.vue'),
+ },
{
path: '/CTS/ManageDomains',
- meta: { layout: ViperLayout },
+ meta: { layout: ViperLayout, permissions: ["SVMSecure.CTS.Manage"] },
component: () => import('@/CTS/pages/ManageDomains.vue'),
},
{
path: '/CTS/ManageEPAs',
- meta: { layout: ViperLayout },
+ meta: { layout: ViperLayout, permissions: ["SVMSecure.CTS.Manage"] },
component: () => import('@/CTS/pages/ManageEpas.vue'),
},
+ {
+ path: '/CTS/ManageMilestones',
+ meta: { layout: ViperLayout, permissions: ["SVMSecure.CTS.Manage"] },
+ component: () => import('@/CTS/pages/ManageMilestones.vue')
+ },
{
path: '/CTS/ManageLevels',
- meta: { layout: ViperLayout },
+ meta: { layout: ViperLayout, permissions: ["SVMSecure.CTS.Manage"] },
component: () => import('@/CTS/pages/ManageLevels.vue'),
},
+ {
+ path: '/CTS/ManageBundles',
+ meta: { layout: ViperLayout, permissions: ["SVMSecure.CTS.Manage"] },
+ component: () => import('@/CTS/pages/ManageBundles.vue'),
+ },
+ {
+ path: '/CTS/ManageBundleCompetencies',
+ meta: { layout: ViperLayout, permissions: ["SVMSecure.CTS.Manage"] },
+ component: () => import('@/CTS/pages/ManageBundleCompetencies.vue'),
+ },
+ {
+ path: '/CTS/ManageRoles',
+ meta: { layout: ViperLayout, permissions: ["SVMSecure.CTS.Manage"] },
+ component: () => import('@/CTS/pages/ManageRoles.vue'),
+ },
{
path: '/CTS/Audit',
name: 'Audit Log',
- meta: { layout: ViperLayout },
+ meta: { layout: ViperLayout, permissions: ["SVMSecure.CTS.Manage"] },
component: () => import('@/CTS/pages/AuditList.vue'),
},
{
path: '/CTS/Test',
name: 'Test Page',
- meta: { layout: ViperLayout },
+ meta: { layout: ViperLayout, permissions: ["SVMSecure.CTS.Manage"] },
component: () => import('@/CTS/pages/TestPage.vue'),
},
/* Reports */
{
path: '/CTS/AssessmentChart',
name: 'AssessmentCharts',
- meta: { layout: ViperLayout },
+ meta: { layout: ViperLayout, permissions: ["SVMSecure.CTS.Manage"] },
component: () => import('@/CTS/pages/AssessmentChart.vue'),
},
{
diff --git a/VueApp/src/CTS/types/index.ts b/VueApp/src/CTS/types/index.ts
index 3fcf7c4..cf02fa3 100644
--- a/VueApp/src/CTS/types/index.ts
+++ b/VueApp/src/CTS/types/index.ts
@@ -1,4 +1,60 @@
-import { computed } from 'vue'
+export type Domain = {
+ domainId: number,
+ name: string,
+ order: number,
+ description: string | null
+}
+
+export type Competency = {
+ competencyId: number | null,
+ domainId: number | null,
+ parentId: number | null,
+ name: string,
+ number: string,
+ description: string | null,
+ canLinkToStudent: boolean,
+ domain: Domain | null,
+ children: Competency[] | null,
+ type: string
+}
+
+export type SessionCompetency = {
+ sessionCompetencyId: number,
+ order: number,
+ sessionId: number,
+ sessionName: string,
+ type: string | null,
+ typeOrder: number,
+ paceOrder: number,
+ multiRole: boolean | null,
+ competencyId: number,
+ competencyNumber: string,
+ competencyName: string,
+ canLinkToStudent: boolean,
+ roleId: number | null,
+ roleName: string | null,
+ levels: Level[],
+}
+
+export type SessionCompetencyAddUpdate = {
+ sessionCompetencyId: number | null,
+ sessionId: number,
+ competencyId: number | null,
+ order: number | null,
+ levelIds: number[],
+ roleId: number | null
+}
+
+export type LegacyComptency = {
+ dvmCompetencyId: number,
+ dvmCompetencyName: string,
+ dvmCompetencyParentId: number | null,
+ dvmCompetencyActive: boolean,
+ levels: Level[],
+ dvmRoleName: string | null,
+ competencies: Competency[],
+}
+
export type Epa = {
epaId: number | null
order: number | null
@@ -30,6 +86,9 @@ export type Assessment = {
epaId: number | null
epaName: string | null
+
+ serviceId: number | null
+ serviceName: string | null
}
export type StudentEpaFormData = {
@@ -86,4 +145,107 @@ export type Person = {
lastName: string,
fullName: string,
fullNameLastFirst: string,
+}
+
+export type Role = {
+ roleId: number,
+ name: string,
+}
+
+export type Bundle = {
+ bundleId: number | null,
+ name: string,
+ clinical: boolean,
+ assessment: boolean,
+ milestone: boolean,
+ roles: Role[]
+}
+
+export type BundleRole = {
+ bundleRoleId: number,
+ bundleId: number,
+ roleId: number,
+}
+
+export type BundleCompetency = {
+ bundleCompetencyId: number,
+ bundleId: number,
+ roleId: number | null,
+ roleName: number | null,
+ levels: Level[],
+ competencyId: number,
+ competencyNumber: string,
+ competencyName: string,
+ description: string | null,
+ canLinkToStudent: boolean,
+ bundleCompetencyGroupId: number | null,
+ order: number,
+}
+
+export type BundleCompetencyAddUpdate = {
+ bundleCompetencyId: number | null,
+ bundleId: number,
+ competencyId: number | null,
+ order: number,
+ levelIds: number[],
+ roleId: number | null,
+ bundleCompetencyGroupId: number | null,
+}
+
+export type BundleCompetencyGroup = {
+ bundleCompetencyGroupId: number | null,
+ name: string,
+ order: number,
+}
+
+export type Milestone = {
+ milestoneId: number,
+ name: string,
+ competencyId: number,
+ competencyName: string,
+}
+
+export type MilestoneLevel = {
+ milestoneLevelId: number | null,
+ milestoneId: number,
+ levelId: number,
+ levelName: string,
+ levelOrder: number,
+ description: string,
+}
+
+export type MilestoneLevelUpdate = {
+ levelId: number,
+ description: string,
+}
+
+
+export type Course = {
+ courseId: number,
+ status: string,
+ title: string,
+ description: string | null,
+ academicYear: string,
+ crn: string | null,
+ courseNum: string,
+ competencyCount: number | null,
+}
+
+export type Session = {
+ sessionId: number,
+ type: string | null,
+ typeDescription: string | null,
+ title: string,
+ courseTitle: string,
+ courseId: number,
+ typeOrder: number | null,
+ paceOrder: number | null,
+ competencyCount: number | null,
+ multiRole: boolean,
+}
+
+export type Term = {
+ termCode: number,
+ academicYear: string,
+ description: string,
}
\ No newline at end of file
diff --git a/VueApp/src/Students/App.vue b/VueApp/src/Students/App.vue
new file mode 100644
index 0000000..e276969
--- /dev/null
+++ b/VueApp/src/Students/App.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
diff --git a/VueApp/src/Students/index.html b/VueApp/src/Students/index.html
new file mode 100644
index 0000000..ec6c1ad
--- /dev/null
+++ b/VueApp/src/Students/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
VIPER - Students
+
+
+
+
+
+
diff --git a/VueApp/src/Students/pages/StudentClassYear.vue b/VueApp/src/Students/pages/StudentClassYear.vue
new file mode 100644
index 0000000..17a7cbb
--- /dev/null
+++ b/VueApp/src/Students/pages/StudentClassYear.vue
@@ -0,0 +1,239 @@
+
+
+
+
+
Student Class Years
+
+
+
+
+
+
+
+ Updating record for {{selectedStudentName}} Class of {{studentClassYear.classYear}}
+ If you change the class year one the current class year, a new record will be created and the current class year one will be marked
+ as inactive with the reasons and term below.
+
+
+
+
+
+ If changing class year, please fill out below.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {return false}">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{props.row.lastName}}, {{props.row.firstName}}
+
+
+
+
+
+
+
+ Ross
+ {{classYear.leftReasonText}}
+
+
+
+
+
+
+
+
+ Ross
+ {{classYear.leftReasonText}}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/VueApp/src/Students/pages/StudentClassYearImport.vue b/VueApp/src/Students/pages/StudentClassYearImport.vue
new file mode 100644
index 0000000..58ed903
--- /dev/null
+++ b/VueApp/src/Students/pages/StudentClassYearImport.vue
@@ -0,0 +1,125 @@
+
+
+
+
+
Student Class Year Import
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{students.length}} Students {{classLevel.value}} in {{term.label}}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{student.lastName}}, {{student.firstName}}
+
+
+
+ Current: Class of {{student.classYear}}
+
+
+
+
+
+ No students found
+
+
+
+
+
diff --git a/VueApp/src/Students/pages/StudentsHome.vue b/VueApp/src/Students/pages/StudentsHome.vue
new file mode 100644
index 0000000..ad429b2
--- /dev/null
+++ b/VueApp/src/Students/pages/StudentsHome.vue
@@ -0,0 +1,5 @@
+
+
+
+ Students Home
+
\ No newline at end of file
diff --git a/VueApp/src/Students/router/index.ts b/VueApp/src/Students/router/index.ts
new file mode 100644
index 0000000..7f01e74
--- /dev/null
+++ b/VueApp/src/Students/router/index.ts
@@ -0,0 +1,17 @@
+import { createRouter, createWebHistory } from 'vue-router'
+import routes from './routes'
+import useRequireLogin from '@/composables/RequireLogin'
+
+const baseUrl = import.meta.env.VITE_VIPER_HOME
+const router = createRouter({
+ scrollBehavior: () => ({ left: 0, top: 0 }),
+ history: createWebHistory(baseUrl),
+ routes,
+})
+
+router.beforeEach(async (to) => {
+ const { requireLogin } = useRequireLogin(to)
+ return requireLogin(true, "SVMSecure.Students")
+})
+
+export default router
\ No newline at end of file
diff --git a/VueApp/src/Students/router/routes.ts b/VueApp/src/Students/router/routes.ts
new file mode 100644
index 0000000..8eb1d36
--- /dev/null
+++ b/VueApp/src/Students/router/routes.ts
@@ -0,0 +1,30 @@
+import ViperLayout from '@/layouts/ViperLayout.vue'
+import ViperLayoutSimple from '@/layouts/ViperLayoutSimple.vue'
+
+const viperURL = import.meta.env.VITE_VIPER_HOME
+
+const routes = [
+ {
+ path: '/Students/',
+ meta: { layout: ViperLayout },
+ component: () => import('@/Students/pages/StudentsHome.vue'),
+ name: "StudentsHome"
+ },
+ {
+ path: '/Students/Home',
+ meta: { layout: ViperLayout },
+ component: () => import('@/Students/pages/StudentsHome.vue'),
+ },
+ {
+ path: '/Students/StudentClassYear',
+ meta: { layout: ViperLayout },
+ component: () => import('@/Students/pages/StudentClassYear.vue'),
+ },
+ {
+ path: '/Students/StudentClassYearImport',
+ meta: { layout: ViperLayout },
+ component: () => import('@/Students/pages/StudentClassYearImport.vue'),
+ },
+]
+
+export default routes
\ No newline at end of file
diff --git a/VueApp/src/Students/students.ts b/VueApp/src/Students/students.ts
new file mode 100644
index 0000000..500d3b3
--- /dev/null
+++ b/VueApp/src/Students/students.ts
@@ -0,0 +1,26 @@
+//import './assets/main.css'
+
+import { createApp } from 'vue'
+import { createPinia } from 'pinia';
+import router from './router'
+import App from './App.vue'
+import { Quasar, Loading, QSpinnerOval } from 'quasar'
+// Import icon libraries
+import '@quasar/extras/material-icons/material-icons.css'
+
+// Import Quasar css
+import 'quasar/dist/quasar.css'
+import { useQuasarConfig } from '@/composables/QuasarConfig'
+
+//import our css
+import '@/assets/site.css'
+
+const { quasarConfig } = useQuasarConfig()
+const pinia = createPinia()
+const app = createApp(App)
+app.provide("apiURL", import.meta.env.VITE_API_URL)
+app.provide("viperOneUrl", import.meta.env.VITE_VIPER_1_HOME)
+app.use(pinia)
+app.use(router)
+app.use(Quasar, quasarConfig)
+app.mount('#myApp')
\ No newline at end of file
diff --git a/VueApp/src/Students/types/index.ts b/VueApp/src/Students/types/index.ts
new file mode 100644
index 0000000..4b1ea9e
--- /dev/null
+++ b/VueApp/src/Students/types/index.ts
@@ -0,0 +1,63 @@
+
+export type Student = {
+ personId: number,
+ mailId: string,
+ lastName: string,
+ firstName: string,
+ middleName: string | null,
+ fullName: string,
+ classLevel: string | null,
+ termCode: number | null,
+ classYear: number | null,
+ email: string,
+ currentClassYear: boolean,
+ active: boolean,
+ classYears: StudentClassYear[],
+
+}
+
+export type StudentClassYear = {
+ studentClassYearId: number,
+ personId: number,
+ classYear: number,
+ active: boolean,
+ graduated: boolean,
+ ross: boolean,
+ leftTerm: number | null,
+ leftReason: number | null,
+ added: Date,
+ addedBy: number | null,
+ updated: Date | null,
+ updatedBy: number | null,
+ comment: string | null,
+ leftReasonText: string | null,
+}
+
+export type StudentClassYearProblem = {
+ personId: number,
+ mailId: string,
+ lastName: string,
+ firstName: string,
+ middleName: string | null,
+ fullName: string,
+ classLevel: string | null,
+ termCode: number | null,
+ classYear: number | null,
+ email: string,
+ currentClassYear: boolean,
+ active: boolean,
+ classYears: StudentClassYear[],
+ expectedClassYear: number | null,
+ problems: string,
+}
+
+export type StudentClassYearUpdate = {
+ studentClassYearId: number,
+ classYear: number | null,
+ personId: number | null,
+ ross: boolean | null,
+ leftReason: number | null,
+ leftTerm: number | null,
+ comment: string | null,
+ active: boolean,
+}
\ No newline at end of file
diff --git a/VueApp/src/assets/site.css b/VueApp/src/assets/site.css
index 6150d96..c5a90d9 100644
--- a/VueApp/src/assets/site.css
+++ b/VueApp/src/assets/site.css
@@ -208,6 +208,8 @@ div.breadcrumbs {
}
/* Heading Styles */
+.q-dialog h2,
+.q-dialog text-h2,
.q-page-container h2,
.q-page-container .text-h2 {
font-size: 1.4rem;
@@ -216,6 +218,8 @@ div.breadcrumbs {
font-weight: bold;
}
+.q-dialog h3,
+.q-dialog text-h3,
.q-page-container h3,
.q-page-container .text-h3 {
font-size: 1.2rem;
@@ -224,7 +228,8 @@ div.breadcrumbs {
font-weight: bold;
}
-
+.q-dialog h4,
+.q-dialog text-h4,
.q-page-container h4,
.q-page-container .text-h4 {
font-size: 1.1rem;
@@ -233,6 +238,8 @@ div.breadcrumbs {
font-weight: bold;
}
+.q-dialog h5,
+.q-dialog text-h5,
.q-page-container h5,
.q-page-container .text-h5 {
font-size: 1rem;
@@ -241,6 +248,8 @@ div.breadcrumbs {
font-weight: bold;
}
+.q-dialog h6,
+.q-dialog text-h6,
.q-page-container h6,
.q-page-container .text-h6 {
font-size: 1rem;
diff --git a/VueApp/src/components/GenericError.vue b/VueApp/src/components/GenericError.vue
index f482efb..58722a5 100644
--- a/VueApp/src/components/GenericError.vue
+++ b/VueApp/src/components/GenericError.vue
@@ -2,7 +2,7 @@
-
+
diff --git a/VueApp/src/composables/CheckPagePermission.ts b/VueApp/src/composables/CheckPagePermission.ts
new file mode 100644
index 0000000..d189b45
--- /dev/null
+++ b/VueApp/src/composables/CheckPagePermission.ts
@@ -0,0 +1,12 @@
+import { useUserStore } from '@/store/UserStore'
+
+export default function checkHasOnePermission(permissions: string[]): boolean {
+ const userStore = useUserStore()
+ const userPermissions = userStore.userInfo.permissions
+ for (const p of permissions) {
+ if (userPermissions.indexOf(p) >= 0) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/VueApp/src/composables/QuasarConfig.ts b/VueApp/src/composables/QuasarConfig.ts
index 92a42cc..e42ab77 100644
--- a/VueApp/src/composables/QuasarConfig.ts
+++ b/VueApp/src/composables/QuasarConfig.ts
@@ -27,8 +27,13 @@ export function useQuasarConfig() {
backgroundColor: "dark",
messageColor: "light",
boxClass: "bg-grey-2 text-grey-9"
- }
- }
+ },
+ animations: 'all',
+ extras: ['material-icons', 'material-symbols-outlined'],
+ framework: {
+ iconSet: 'material-symbols-outlined'
+ },
+ },
}
return { quasarConfig }
diff --git a/VueApp/src/composables/ViperFetch.ts b/VueApp/src/composables/ViperFetch.ts
index b528aae..6704adf 100644
--- a/VueApp/src/composables/ViperFetch.ts
+++ b/VueApp/src/composables/ViperFetch.ts
@@ -142,12 +142,21 @@ export function useFetch() {
isAuthError = true
}
else {
- result = await response.json()
- message = result.errorMessage != null
- ? result.errorMessage
- : result.detail != null
- ? result.detail
- : result.statusText
+ try {
+ result = await response.json()
+ }
+ catch (e) {
+ result = response
+ }
+ if (result.errorMessage != null) {
+ message = result.errorMessage
+ }
+ else if (result.detail != null) {
+ message = result.detail
+ }
+ else if (result.statusText != null) {
+ message = result.statusText
+ }
}
}
catch (e) {
diff --git a/VueApp/vite.config.ts b/VueApp/vite.config.ts
index 6546da8..1cc4c8a 100644
--- a/VueApp/vite.config.ts
+++ b/VueApp/vite.config.ts
@@ -74,6 +74,7 @@ export default defineConfig(({ mode }) => ({
main: resolve(__dirname, 'index.html'),
cts: resolve(__dirname, 'src/cts/index.html'),
computing: resolve(__dirname, 'src/computing/index.html'),
+ students: resolve(__dirname, 'src/students/index.html'),
}
}
},
diff --git a/VueApp/vueapp.esproj b/VueApp/vueapp.esproj
index dda6a1e..b683789 100644
--- a/VueApp/vueapp.esproj
+++ b/VueApp/vueapp.esproj
@@ -11,8 +11,11 @@
+
+
+
\ No newline at end of file
diff --git a/web/Areas/CTS/Controllers/AssessmentController.cs b/web/Areas/CTS/Controllers/AssessmentController.cs
index 81020ed..df02d91 100644
--- a/web/Areas/CTS/Controllers/AssessmentController.cs
+++ b/web/Areas/CTS/Controllers/AssessmentController.cs
@@ -16,12 +16,14 @@ namespace Viper.Areas.CTS.Controllers
public class AssessmentController : ApiController
{
private readonly VIPERContext context;
- private AuditService auditService;
- private CtsSecurityService ctsSecurityService;
+ private readonly RAPSContext rapsContext;
+ private readonly AuditService auditService;
+ private readonly CtsSecurityService ctsSecurityService;
public AssessmentController(VIPERContext _context, RAPSContext rapsContext)
{
context = _context;
+ this.rapsContext = rapsContext;
auditService = new AuditService(context);
ctsSecurityService = new CtsSecurityService(rapsContext, _context);
}
@@ -46,7 +48,7 @@ public async Task>> GetAssessments(int? typ
{
if (!ctsSecurityService.CheckStudentAssessmentViewAccess(studentUserId, enteredById))
{
- return Forbid();
+ return (ActionResult>)ForbidApi();
}
var assessments = context.Encounters
@@ -91,15 +93,16 @@ public async Task>> GetAssessments(int? typ
switch (sortBy.ToLower())
{
case "enteredon": assessments = descending ? assessments.OrderByDescending(a => a.EnteredOn) : assessments.OrderBy(a => a.EnteredOn); break;
- case "enteredbyname":
+ case "enteredbyname":
assessments = descending
? assessments.OrderByDescending(a => a.EnteredByPerson.LastName)
.ThenByDescending(a => a.EnteredByPerson.FirstName)
: assessments.OrderBy(a => a.EnteredByPerson.LastName)
.ThenBy(a => a.EnteredByPerson.FirstName);
break;
- case "levelname": assessments = descending
- ? assessments.OrderByDescending(a => a.Level != null ? a.Level.LevelName : "")
+ case "levelname":
+ assessments = descending
+ ? assessments.OrderByDescending(a => a.Level != null ? a.Level.LevelName : "")
: assessments.OrderBy(a => a.Level != null ? a.Level.LevelName : ""); break;
case "servicename":
assessments = descending
@@ -115,7 +118,7 @@ public async Task>> GetAssessments(int? typ
.ThenByDescending(a => a.Student.FirstName)
: assessments.OrderBy(a => a.Student.LastName)
.ThenBy(a => a.Student.FirstName);
- break;
+ break;
}
}
if (pagination != null)
@@ -145,6 +148,7 @@ public async Task>> GetAssessments(int? typ
}
[HttpGet("assessors")]
+ [Permission(Allow = "SVMSecure.CTS.Manage,SVMSecure.CTS.StudentAssessments,SVMSecure.CTS.AssessClinical")]
public async Task>> GetAssessors(int? type, int? serviceId)
{
var encounters = context.Encounters.AsQueryable();
@@ -199,13 +203,63 @@ public async Task> GetStudentAssessment(int enco
}
if (!ctsSecurityService.CheckStudentAssessmentViewAccess(encounter.StudentUserId, encounter.EnteredBy))
{
- return Forbid();
+ return (ActionResult)ForbidApi();
}
var sa = CreateStudentAssessment(encounter);
sa.Editable = ctsSecurityService.CanEditStudentAssessment(sa.EnteredBy);
return sa;
}
+ ///
+ /// Given an Eval360 instance id, get the list of student evalautees for this evaluator and whether or not they have an EPA during
+ /// this rotation.
+ ///
+ ///
+ ///
+ [HttpGet("epacompletion")]
+ [Permission(Allow = "SVMSecure")]
+ public async Task>> EvalauteeStudentsWithEpas(int instanceId)
+ {
+ List evaluateesWithCompletion = new();
+ var userHelper = new UserHelper();
+ var user = userHelper.GetCurrentUser();
+ //get instance and check to make sure it belongs to the logged in user, or the logged in user has an admin permission
+ var instance = await context.Instances.FindAsync(instanceId);
+ if (instance == null)
+ {
+ return NotFound();
+ }
+
+ if (instance.InstanceMothraId != user?.MothraId
+ && !userHelper.HasPermission(rapsContext, user, "SVMSecure.Eval.ViewStudentClinResultsAll")
+ && !userHelper.HasPermission(rapsContext, user, "SVMSecure.CTS.Manage"))
+ {
+ return Forbid();
+ }
+
+ //get evaluatees for this eval that are on the rotation for at least one week the logged in user is on this rotation
+ var evaluatees = await context.EvaluateesByInstances
+ .Where(e => e.InstanceId == instanceId)
+ .OrderBy(e => e.LastName)
+ .ThenBy(e => e.FirstName)
+ .ThenBy(e => e.PersonId)
+ .ToListAsync();
+
+ //for each evaluatee, get the epas for this service that are dated within the block start/end, and mark that an EPA has been done or not done
+ foreach (var e in evaluatees)
+ {
+ var epaAssessments = await context.Encounters
+ .Where(enc => enc.EncounterType == (int)EncounterCreationService.EncounterType.Epa)
+ .Where(enc => enc.EncounterDate >= e.StartDate && enc.EncounterDate <= e.EndDate)
+ .Where(enc => enc.Student.PersonId == e.PersonId)
+ .Where(enc => enc.ServiceId == e.ServiceId)
+ .CountAsync();
+ evaluateesWithCompletion.Add(new(e, epaAssessments > 0));
+ }
+
+ return evaluateesWithCompletion;
+ }
+
///
/// Create a new epa assessment
///
diff --git a/web/Areas/CTS/Controllers/BundleCompetencyController.cs b/web/Areas/CTS/Controllers/BundleCompetencyController.cs
new file mode 100644
index 0000000..6fcce83
--- /dev/null
+++ b/web/Areas/CTS/Controllers/BundleCompetencyController.cs
@@ -0,0 +1,209 @@
+using AutoMapper;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using System.Diagnostics;
+using System.Reflection.Metadata;
+using Viper.Areas.CTS.Models;
+using Viper.Classes;
+using Viper.Classes.SQLContext;
+using Viper.Models.CTS;
+using Web.Authorization;
+
+namespace Viper.Areas.CTS.Controllers
+{
+ [Route("/api/cts/bundles/{bundleId}/competencies")]
+ [Permission(Allow = "SVMSecure.CTS")]
+ public class BundleCompetencyController : ApiController
+ {
+ private readonly IMapper mapper;
+ private readonly VIPERContext context;
+
+ public BundleCompetencyController(IMapper mapper, VIPERContext context)
+ {
+ this.mapper = mapper;
+ this.context = context;
+ }
+
+ private bool BundleExists(int bundleId)
+ {
+ return context.Bundles.Where(b => b.BundleId == bundleId).Any();
+ }
+
+ private bool CompetencyExists(int competencyId)
+ {
+ return context.Competencies.Where(b => b.CompetencyId == competencyId).Any();
+ }
+
+ [HttpGet]
+ public async Task>> GetBundleCompetencies(int bundleId)
+ {
+ if (!BundleExists(bundleId))
+ {
+ return NotFound();
+ }
+
+ var bundleComps = await context.BundleCompetencies
+ .Include(bc => bc.Competency)
+ .Include(bc => bc.Role)
+ .Include(bc => bc.BundleCompetencyGroup)
+ .Include(bc => bc.BundleCompetencyLevels)
+ .ThenInclude(bcl => bcl.Level)
+ .Where(bc => bc.BundleId == bundleId)
+ .OrderBy(bc => bc.BundleCompetencyGroup == null ? 0 : bc.BundleCompetencyGroup.Order)
+ .ThenBy(bc => bc.Order)
+ .ThenBy(bc => bc.Competency.Name)
+ .ToListAsync();
+
+ return mapper.Map>(bundleComps);
+ }
+
+ [HttpPost]
+ [Permission(Allow = "SVMSecure.CTS.Manage")]
+ public async Task> AddBundleCompetency(int bundleId, BundleCompetencyAddUpdate bundleComp)
+ {
+ if (!BundleExists(bundleId) || !CompetencyExists(bundleComp.CompetencyId))
+ {
+ return NotFound();
+ }
+ var compExistsAlready = await context.BundleCompetencies
+ .Where(bc => bc.BundleId == bundleId && bc.CompetencyId == bundleComp.CompetencyId && bc.RoleId == bundleComp.RoleId)
+ .AnyAsync();
+ if(compExistsAlready)
+ {
+ return BadRequest("Competency is already a part of this bundle.");
+ }
+
+ var bundleCompetency = new BundleCompetency()
+ {
+ BundleId = bundleId,
+ CompetencyId = bundleComp.CompetencyId,
+ BundleCompetencyGroupId = bundleComp.BundleCompetencyGroupId,
+ Order = bundleComp.Order,
+ RoleId = bundleComp.RoleId
+ };
+
+ //Create the bundle competency and add levels
+ using var trans = context.Database.BeginTransaction();
+ context.Add(bundleCompetency);
+ await context.SaveChangesAsync();
+
+ foreach (var levelId in bundleComp.LevelIds)
+ {
+ var level = await context.Levels.FindAsync(levelId);
+ if (level != null)
+ {
+ context.Add(new BundleCompetencyLevel()
+ {
+ BundleCompetencyId = bundleCompetency.BundleCompetencyId,
+ LevelId = levelId
+ });
+ }
+ }
+ await context.SaveChangesAsync();
+ AdjustBundleCompetencyOrders(bundleCompetency);
+ await trans.CommitAsync();
+
+ return mapper.Map(bundleCompetency);
+ }
+
+ [HttpPut("{bundleCompetencyId}")]
+ [Permission(Allow = "SVMSecure.CTS.Manage")]
+ public async Task> UpdateBundleCompetency(int bundleId, int bundleCompetencyId, BundleCompetencyAddUpdate bundleCompetency)
+ {
+ var bundleComp = context.BundleCompetencies.Find(bundleCompetencyId);
+ if (bundleComp == null)
+ {
+ return NotFound();
+ }
+ if (!BundleExists(bundleId) || !CompetencyExists(bundleCompetency.CompetencyId))
+ {
+ return NotFound();
+ }
+
+ using var trans = context.Database.BeginTransaction();
+ bundleComp.Order = bundleCompetency.Order;
+ bundleComp.RoleId = bundleCompetency.RoleId;
+ var existingLevels = await context.BundleCompetencyLevels.Where(bcl => bcl.BundleCompetencyId == bundleCompetencyId).ToListAsync();
+ foreach (var existingLevel in existingLevels)
+ {
+ if (!bundleCompetency.LevelIds.Any(l => l == existingLevel.LevelId))
+ {
+ context.BundleCompetencyLevels.Remove(existingLevel);
+ }
+ }
+ foreach (var newLevel in bundleCompetency.LevelIds)
+ {
+ if (!bundleComp.BundleCompetencyLevels.Any(bcl => bcl.LevelId == newLevel))
+ {
+ context.Add(new BundleCompetencyLevel()
+ {
+ BundleCompetencyId = bundleCompetencyId,
+ LevelId = newLevel
+ });
+ }
+ }
+ await context.SaveChangesAsync();
+ AdjustBundleCompetencyOrders(bundleComp);
+ await trans.CommitAsync();
+
+ return mapper.Map(bundleComp);
+ }
+
+ [HttpDelete("{bundleCompetencyId}")]
+ [Permission(Allow = "SVMSecure.CTS.Manage")]
+ public async Task> DeleteBundleCompetency(int bundleId, int bundleCompetencyId)
+ {
+ var bundleComp = context.BundleCompetencies.Find(bundleCompetencyId);
+ if (bundleComp == null)
+ {
+ return NotFound();
+ }
+ if (!BundleExists(bundleId))
+ {
+ return NotFound();
+ }
+
+ try
+ {
+ context.BundleCompetencies.Remove(bundleComp);
+ await context.SaveChangesAsync();
+ AdjustBundleCompetencyOrders(bundleComp);
+ }
+ catch (Exception)
+ {
+ return BadRequest("Cannot delete this bundle competency.");
+ }
+
+ return mapper.Map(bundleComp);
+ }
+
+ private void AdjustBundleCompetencyOrders(BundleCompetency bundleComp)
+ {
+ var bundleComps = context.BundleCompetencies
+ .Where(b => b.BundleId == bundleComp.BundleId)
+ .Where(b => b.BundleCompetencyGroupId == bundleComp.BundleCompetencyGroupId)
+ .OrderBy(b => b.Order)
+ .ToList();
+
+ //check orders are correct
+ int i = 1;
+ bool changeMade = false;
+ foreach (var b in bundleComps)
+ {
+ if (b.Order != i)
+ {
+ //not correct
+ b.Order = i;
+ context.Entry(b).State = EntityState.Modified;
+ changeMade = true;
+ }
+ i++;
+ }
+
+ if (changeMade)
+ {
+ context.SaveChanges();
+ }
+ }
+ }
+}
diff --git a/web/Areas/CTS/Controllers/BundleCompetencyGroupController.cs b/web/Areas/CTS/Controllers/BundleCompetencyGroupController.cs
new file mode 100644
index 0000000..1e46fc1
--- /dev/null
+++ b/web/Areas/CTS/Controllers/BundleCompetencyGroupController.cs
@@ -0,0 +1,156 @@
+using AutoMapper;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Internal;
+using Viper.Areas.CTS.Models;
+using Viper.Classes;
+using Viper.Classes.SQLContext;
+using Viper.Models.CTS;
+using Web.Authorization;
+
+namespace Viper.Areas.CTS.Controllers
+{
+ [Route("/api/cts/bundles/{bundleId}/groups")]
+ [Permission(Allow = "SVMSecure.CTS")]
+ public class BundleCompetencyGroupController : ApiController
+ {
+ private readonly IMapper mapper;
+ private readonly VIPERContext context;
+ public BundleCompetencyGroupController(IMapper mapper, VIPERContext context)
+ {
+ this.mapper = mapper;
+ this.context = context;
+ }
+
+ private bool BundleExists(int bundleId)
+ {
+ return context.Bundles.Where(b => b.BundleId == bundleId).Any();
+ }
+
+ private bool SameNameExists(int bundleId, string name, int? bundleCompetencyGroupId = null)
+ {
+ return context.BundleCompetencyGroups
+ .Where(g => g.BundleId == bundleId
+ && g.Name == name
+ && (bundleCompetencyGroupId == null || bundleCompetencyGroupId != g.BundleCompetencyGroupId))
+ .Any();
+ }
+
+ [HttpGet]
+ public async Task>> GetGroups(int bundleId)
+ {
+ if (!BundleExists(bundleId))
+ {
+ return NotFound();
+ }
+ var groups = await context.BundleCompetencyGroups
+ .Where(g => g.BundleId == bundleId)
+ .ToListAsync();
+ return mapper.Map>(groups);
+ }
+
+ [HttpPost]
+ [Permission(Allow = "SVMSecure.CTS.Manage")]
+ public async Task> AddGroup(int bundleId, BundleCompetencyGroupDto groupDto)
+ {
+ if (!BundleExists(bundleId))
+ {
+ return NotFound();
+ }
+ if (SameNameExists(bundleId, groupDto.Name))
+ {
+ return BadRequest("Name must be unique");
+ }
+
+ var group = new BundleCompetencyGroup()
+ {
+ BundleId = bundleId,
+ Name = groupDto.Name,
+ Order = groupDto.Order
+ };
+ context.BundleCompetencyGroups.Add(group);
+ await context.SaveChangesAsync();
+ AdjustGroupOrders(group);
+ return mapper.Map(group);
+ }
+
+ [HttpPut("{bundleCompetencyGroupId}")]
+ [Permission(Allow = "SVMSecure.CTS.Manage")]
+ public async Task> UpdateGroup(int bundleId, int bundleCompetencyGroupId, BundleCompetencyGroupDto groupDto)
+ {
+ if (!BundleExists(bundleId))
+ {
+ return NotFound();
+ }
+ var group = await context.BundleCompetencyGroups.FindAsync(bundleCompetencyGroupId);
+ if (group == null)
+ {
+ return NotFound();
+ }
+ if (SameNameExists(bundleId, groupDto.Name, bundleCompetencyGroupId))
+ {
+ return BadRequest("Name must be unique");
+ }
+
+ group.Name = groupDto.Name;
+ group.Order = groupDto.Order;
+ context.Update(group);
+ await context.SaveChangesAsync();
+ AdjustGroupOrders(group);
+ return mapper.Map(group);
+ }
+
+ [HttpDelete("{bundleCompetencyGroupId}")]
+ [Permission(Allow = "SVMSecure.CTS.Manage")]
+ public async Task> DeleteGroup(int bundleId, int bundleCompetencyGroupId)
+ {
+ if (!BundleExists(bundleId))
+ {
+ return NotFound();
+ }
+ var group = await context.BundleCompetencyGroups.FindAsync(bundleCompetencyGroupId);
+ if (group == null)
+ {
+ return NotFound();
+ }
+ try
+ {
+ context.Remove(group);
+ await context.SaveChangesAsync();
+ AdjustGroupOrders(group);
+ }
+ catch (Exception)
+ {
+ return BadRequest("Cannot delete group. Competencies must be removed from the group, and the group cannot have been used to document a student competency.");
+ }
+ return mapper.Map(group);
+ }
+
+ private void AdjustGroupOrders(BundleCompetencyGroup group)
+ {
+ var groups = context.BundleCompetencyGroups
+ .Where(g => g.BundleId == group.BundleId)
+ .OrderBy(g => g.Order)
+ .ToList();
+
+ int i = 1;
+ bool changeMade = false;
+ foreach (var g in groups)
+ {
+ if (g.Order != i)
+ {
+ //not correct
+ g.Order = i;
+ context.Entry(g).State = EntityState.Modified;
+ changeMade = true;
+ }
+ i++;
+ }
+
+ if (changeMade)
+ {
+ context.SaveChanges();
+ }
+ }
+ }
+}
diff --git a/web/Areas/CTS/Controllers/BundleController.cs b/web/Areas/CTS/Controllers/BundleController.cs
new file mode 100644
index 0000000..2f3e37b
--- /dev/null
+++ b/web/Areas/CTS/Controllers/BundleController.cs
@@ -0,0 +1,186 @@
+using AutoMapper;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using System.Data;
+using Viper.Areas.CTS.Models;
+using Viper.Classes;
+using Viper.Classes.SQLContext;
+using Viper.Models.CTS;
+using Web.Authorization;
+
+namespace Viper.Areas.CTS.Controllers
+{
+ [Route("/api/cts/bundles/")]
+ [Permission(Allow = "SVMSecure.CTS")]
+ public class BundleController : ApiController
+ {
+ private readonly IMapper mapper;
+ private readonly VIPERContext context;
+
+ public BundleController(IMapper mapper, VIPERContext context)
+ {
+ this.mapper = mapper;
+ this.context = context;
+ }
+
+ [HttpGet]
+ public async Task>> GetBundles(bool? clinical = null, bool? assessment = null, bool? milestone = null,
+ int? serviceId = null, int? roleId = null)
+ {
+ var bundleQuery = context.Bundles
+ .Include(b => b.BundleRoles)
+ .ThenInclude(br => br.Role)
+ .Include(b => b.BundleCompetencies)
+ .AsQueryable();
+ if (clinical != null)
+ {
+ bundleQuery = bundleQuery.Where(b => b.Clinical == clinical);
+ }
+ if (assessment != null)
+ {
+ bundleQuery = bundleQuery.Where(b => b.Assessment == assessment);
+ }
+ if (milestone != null)
+ {
+ bundleQuery = bundleQuery.Where(b => b.Milestone == milestone);
+ }
+ if (serviceId != null)
+ {
+ bundleQuery = bundleQuery.Where(b => b.BundleServices.Any(s => s.ServiceId == serviceId));
+ }
+ if (roleId != null)
+ {
+ bundleQuery = bundleQuery.Where(b => b.BundleRoles.Any(br => br.RoleId == roleId));
+ }
+
+ var bundles = await bundleQuery
+ .OrderBy(b => b.Name)
+ .ToListAsync();
+
+ return mapper.Map>(bundles);
+ }
+
+ [HttpGet("{bundleId}")]
+ public async Task> GetBundle(int bundleId)
+ {
+ var bundle = await context.Bundles
+ .Include(b => b.BundleRoles)
+ .ThenInclude(br => br.Role)
+ .Where(b => b.BundleId == bundleId)
+ .FirstOrDefaultAsync();
+ if (bundle == null)
+ {
+ return NotFound();
+ }
+ return mapper.Map(bundle);
+ }
+
+ [HttpPost]
+ [Permission(Allow = "SVMSecure.CTS.Manage")]
+ public async Task> AddBundle(BundleDto bundleDto)
+ {
+ if (string.IsNullOrEmpty(bundleDto.Name))
+ {
+ return BadRequest("Bundle Name is required.");
+ }
+ var nameCheck = await context.Bundles.Where(b => b.Name == bundleDto.Name).AnyAsync();
+ if (nameCheck)
+ {
+ return BadRequest("Bundle Name must be unique.");
+ }
+
+ Bundle b = mapper.Map(bundleDto);
+ context.Add(b);
+ await context.SaveChangesAsync();
+
+ return mapper.Map(b);
+ }
+
+ [HttpPut("{bundleId}")]
+ [Permission(Allow = "SVMSecure.CTS.Manage")]
+ public async Task> UpdateBundle(int bundleId, BundleDto bundleDto)
+ {
+ var exists = await context.Bundles.AnyAsync(b => b.BundleId == bundleId);
+ if (!exists)
+ {
+ return NotFound();
+ }
+ if (string.IsNullOrEmpty(bundleDto.Name))
+ {
+ return BadRequest("Bundle Name is required.");
+ }
+ var nameCheck = await context.Bundles.Where(b => b.Name == bundleDto.Name && b.BundleId != bundleId).AnyAsync();
+ if (nameCheck)
+ {
+ return BadRequest("Bundle Name must be unique.");
+ }
+ Bundle b = mapper.Map(bundleDto);
+ context.Update(b);
+ await context.SaveChangesAsync();
+ return mapper.Map(b);
+ }
+
+ [HttpDelete("{bundleId}")]
+ [Permission(Allow = "SVMSecure.CTS.Manage")]
+ public async Task> DeleteBundle(int bundleId)
+ {
+ var bundle = await context.Bundles.FindAsync(bundleId);
+ if (bundle == null)
+ {
+ return NotFound();
+ }
+
+ try
+ {
+ using var trans = context.Database.BeginTransaction();
+ var bundleRoles = context.BundleRoles.Where(br => br.BundleId == bundleId);
+ foreach (var role in bundleRoles)
+ {
+ context.Remove(role);
+ }
+ context.Entry(bundle).State = EntityState.Deleted;
+ await context.SaveChangesAsync();
+ await trans.CommitAsync();
+ }
+ catch (Exception)
+ {
+ return BadRequest("Could not delete bundle. If this bundle has been used, it cannot be deleted.");
+ }
+ return mapper.Map(bundle);
+ }
+
+ /* Bundle Roles */
+ [HttpPut("{bundleId}/roles/")]
+ [Permission(Allow = "SVMSecure.CTS.Manage")]
+ public async Task>> SetBundleRoles(int bundleId, List bundleRoles)
+ {
+ using var trans = context.Database.BeginTransaction();
+ var existing = await context.BundleRoles.Where(br => br.BundleId == bundleId).ToListAsync();
+ foreach (var brId in bundleRoles)
+ {
+ if (!existing.Any(e => e.RoleId == brId))
+ {
+ context.Add(new BundleRole()
+ {
+ BundleId = bundleId,
+ RoleId = brId
+ });
+ }
+ }
+
+ foreach (var e in existing)
+ {
+ if (!bundleRoles.Any(brId => brId == e.RoleId))
+ {
+ context.Entry(e).State = EntityState.Deleted;
+ }
+ }
+
+ await context.SaveChangesAsync();
+ await trans.CommitAsync();
+
+ var roles = await context.BundleRoles.Where(br => br.BundleId == bundleId).Select(br => br.Role).ToListAsync();
+ return mapper.Map>(roles);
+ }
+ }
+}
diff --git a/web/Areas/CTS/Controllers/ClinicalScheduleController.cs b/web/Areas/CTS/Controllers/ClinicalScheduleController.cs
index f520fb0..731443d 100644
--- a/web/Areas/CTS/Controllers/ClinicalScheduleController.cs
+++ b/web/Areas/CTS/Controllers/ClinicalScheduleController.cs
@@ -42,7 +42,7 @@ public async Task>> GetStudentSchedu
{
if (!clinicalScheduleSecurity.CheckStudentScheduleParams(mothraId, rotationId, serviceId, weekId, startDate, endDate))
{
- return Forbid();
+ return ForbidApi();
}
var schedule = await clinicalSchedule.GetStudentSchedule(classYear, mothraId, rotationId, serviceId, weekId, startDate, endDate);
return schedule;
@@ -64,7 +64,7 @@ public async Task>> GetInstructorSchedule(
{
if (!clinicalScheduleSecurity.CheckInstructorScheduleParams(mothraId, rotationId, serviceId, weekId, startDate, endDate))
{
- return Forbid();
+ return ForbidApi();
}
var schedule = await clinicalSchedule.GetInstructorSchedule(classYear, mothraId, rotationId, serviceId, weekId, startDate, endDate, active);
return schedule;
diff --git a/web/Areas/CTS/Controllers/CompetencyController.cs b/web/Areas/CTS/Controllers/CompetencyController.cs
new file mode 100644
index 0000000..6c7fd27
--- /dev/null
+++ b/web/Areas/CTS/Controllers/CompetencyController.cs
@@ -0,0 +1,237 @@
+using Viper.Classes;
+using Web.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Viper.Classes.SQLContext;
+using Microsoft.EntityFrameworkCore;
+using Viper.Models.CTS;
+using Viper.Areas.CTS.Models;
+using System.DirectoryServices.ActiveDirectory;
+using AutoMapper;
+
+namespace Viper.Areas.CTS.Controllers
+{
+ [Route("/api/cts/competencies/")]
+ [Permission(Allow = "SVMSecure.CTS")]
+ public class CompetencyController : ApiController
+ {
+ private readonly VIPERContext context;
+ private readonly IMapper mapper;
+
+ public CompetencyController(VIPERContext context, IMapper mapper)
+ {
+ this.context = context;
+ this.mapper = mapper;
+ }
+
+ [HttpGet]
+ public async Task>> Index()
+ {
+ return await context.Competencies
+ .Include(c => c.Domain)
+ .OrderBy(c => c.Order)
+ .Select(c => new CompetencyDto(c))
+ .ToListAsync();
+ }
+
+ [HttpGet("{competencyId}")]
+ public async Task> GetComp(int competencyId)
+ {
+ var comp = await context.Competencies.Include(c => c.Domain).Where(c => c.CompetencyId == competencyId).FirstOrDefaultAsync();
+ if (comp == null)
+ {
+ return NotFound();
+ }
+ return new CompetencyDto(comp);
+ }
+
+ [HttpGet("{competencyId}/children")]
+ public async Task>> GetChildren(int competencyId)
+ {
+ var comps = await context.Competencies
+ .Include(c => c.Domain)
+ .Where(c => c.ParentId == competencyId)
+ .OrderBy(c => c.Order)
+ .Select(c => new CompetencyDto(c))
+ .ToListAsync();
+ if (comps.Count == 0)
+ {
+ return NotFound();
+ }
+ return comps;
+ }
+
+
+ [HttpGet("hierarchy")]
+ [Permission(Allow = "SVMSecure.CTS.Manage")]
+ public async Task>> GetCompetencyHierarchy()
+ {
+ var comps = await context.Competencies
+ .Include(c => c.Domain)
+ .OrderBy(c => c.Domain.Order)
+ .ThenBy(c => c.Order)
+ .ToListAsync();
+ var compHierarchy = new List();
+ var allCompDtos = mapper.Map>(comps);
+ foreach (var comp in allCompDtos)
+ {
+ if (comp.ParentId == null)
+ {
+ compHierarchy.Add(comp);
+ }
+ else
+ {
+ var parent = allCompDtos.Where(c => c.CompetencyId == comp.ParentId).FirstOrDefault();
+ if (parent != null)
+ {
+ parent.Children.Append(comp);
+ }
+ }
+ }
+ return compHierarchy;
+ }
+
+ [HttpPost]
+ [Permission(Allow = "SVMSecure.CTS.Manage")]
+ public async Task> AddCompetency(CompetencyAddUpdate competency)
+ {
+ if (competency.CompetencyId != null)
+ {
+ return BadRequest("CompetencyId cannot be set for a new competency.");
+ }
+
+ if (competency.Name.Length < 2 || competency.Number.Length < 1)
+ {
+ return BadRequest("Name and Number are required.");
+ }
+
+ var duplicates = await context.Competencies.Where(c => c.Number == competency.Number).ToListAsync();
+ if (duplicates.Count > 0)
+ {
+ return BadRequest("A competency with this number exists already.");
+ }
+
+ var comp = new Competency()
+ {
+ Name = competency.Name,
+ Number = competency.Number,
+ CanLinkToStudent = competency.CanLinkToStudent,
+ Description = competency.Description,
+ DomainId = competency.DomainId,
+ ParentId = competency.ParentId
+ };
+ context.Competencies.Add(comp);
+ await context.SaveChangesAsync();
+ await UpdateCompetencyOrders();
+ return await GetComp(comp.CompetencyId);
+ }
+
+ [HttpPut("{competencyId}")]
+ [Permission(Allow = "SVMSecure.CTS.Manage")]
+ public async Task> UpdateComptency(int competencyId, CompetencyAddUpdate competency)
+ {
+ if (competencyId != competency.CompetencyId)
+ {
+ return BadRequest();
+ }
+
+ var c = await context.Competencies.FindAsync(competency.CompetencyId);
+ if (c == null)
+ {
+ return NotFound();
+ }
+
+ if (competency.CompetencyId == null || competency.CompetencyId <= 0)
+ {
+ return BadRequest("CompetencyId is required.");
+ }
+ if (competency.Name.Length < 2 || competency.Number.Length < 1)
+ {
+ return BadRequest("Name and Number are required.");
+ }
+
+ var duplicates = await context.Competencies
+ .Where(c => c.CompetencyId != competency.CompetencyId)
+ .Where(c => c.Number == competency.Number)
+ .ToListAsync();
+ if (duplicates.Count > 0)
+ {
+ return BadRequest("A competency with this number exists already.");
+ }
+
+ c.Name = competency.Name;
+ c.Number = competency.Number;
+ c.Description = competency.Description;
+ c.CanLinkToStudent = competency.CanLinkToStudent;
+ c.ParentId = competency.ParentId;
+
+ context.Competencies.Update(c);
+ await context.SaveChangesAsync();
+ await UpdateCompetencyOrders();
+ return await GetComp(c.CompetencyId);
+ }
+
+ [HttpDelete("{competencyId}")]
+ [Permission(Allow = "SVMSecure.CTS.Manage")]
+ public async Task> DeleteCompetency(int competencyId)
+ {
+ var c = await context.Competencies.FindAsync(competencyId);
+ if (c == null)
+ {
+ return NotFound();
+ }
+ context.Competencies.Remove(c);
+ try
+ {
+ await context.SaveChangesAsync();
+ }
+ catch (Exception)
+ {
+ return BadRequest("Could not remove domain. It may be linked to other objects.");
+ }
+ await UpdateCompetencyOrders();
+ return new CompetencyDto(c);
+ }
+
+ private async Task UpdateCompetencyOrders()
+ {
+ var comps = await context.Competencies
+ .ToListAsync();
+
+ comps = comps.OrderBy(c =>
+ {
+ var numbers = c.Number.Split(".");
+ var retval = 0;
+ if (numbers.Length > 0 && int.TryParse(numbers[0], out int num1))
+ {
+ retval += num1 * 1000000;
+ }
+ if (numbers.Length > 1 && int.TryParse(numbers[1], out int num2))
+ {
+ retval += num2 * 10000;
+ }
+ if (numbers.Length > 2 && int.TryParse(numbers[2], out int num3))
+ {
+ retval += num3 * 100;
+ }
+ if (numbers.Length > 3 && int.TryParse(numbers[3], out int num4))
+ {
+ retval += num4;
+ }
+ return retval;
+ }).ToList();
+
+ int order = 1;
+ foreach (var comp in comps)
+ {
+ if (comp.Order != order)
+ {
+ comp.Order = order;
+ context.Update(comp);
+ }
+ order++;
+ }
+
+ await context.SaveChangesAsync();
+ }
+ }
+}
diff --git a/web/Areas/CTS/Controllers/CourseController.cs b/web/Areas/CTS/Controllers/CourseController.cs
index 11c1f03..076a7bc 100644
--- a/web/Areas/CTS/Controllers/CourseController.cs
+++ b/web/Areas/CTS/Controllers/CourseController.cs
@@ -1,39 +1,507 @@
-using Microsoft.AspNetCore.Mvc;
+using Amazon.SimpleSystemsManagement.Model;
+using AutoMapper;
+using Microsoft.AspNetCore.JsonPatch.Internal;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Internal;
+using Polly.Caching;
+using System.Data;
using Viper.Areas.CTS.Models;
using Viper.Areas.CTS.Services;
using Viper.Classes;
using Viper.Classes.SQLContext;
+using Viper.Models.CTS;
using Web.Authorization;
+using static System.Runtime.CompilerServices.RuntimeHelpers;
namespace Viper.Areas.CTS.Controllers
{
- [Route("/api/cts/courses/")]
- [Permission(Allow = "SVMSecure")]
- public class CourseController : ApiController
- {
- private readonly CrestCourseService courseService;
-
- public CourseController(VIPERContext context)
- {
- courseService = new(context);
- }
-
- [HttpGet]
- public async Task>> GetCourses(string? termCode, string? subjectCode, string? courseNum)
- {
- return await courseService.GetCourses(termCode: termCode, subjectCode: subjectCode, courseNum: courseNum);
- }
-
- [HttpGet]
- [Route("/cts/courses/{courseId}")]
- public async Task> GetCourse(int courseId)
- {
- var c = await courseService.GetCourse(courseId);
- if(c == null)
- {
- return NotFound();
- }
- return c;
- }
- }
+ /*
+ * Permissions for viewing course + session competency info:
+ * Faculty can view courses they are the leader of or an instructor for
+ * Dept proxies can view their courses
+ * CTS.Manage and CTS.LoginStudents can view all courses
+ * Students cannot view courses
+ */
+ [Route("/api/cts/courses/")]
+ [Permission(Allow = "SVMSecure.CTS", Deny = "SVMSecure.CTS.Student")]
+ public class CourseController : ApiController
+ {
+ private readonly CrestCourseService courseService;
+ private readonly RAPSContext rapsContext;
+ private readonly VIPERContext context;
+ private readonly IMapper mapper;
+ private readonly List CompetencySupportedSessionTypes = new List()
+ {
+ "Lab", "L/D", "Dis","Exm","AUT","JLC","PRS","CBL","PBL","D/L","TBL","ACT"
+ };
+
+ public CourseController(IMapper mapper, VIPERContext context, RAPSContext rapsContext)
+ {
+ this.mapper = mapper;
+ this.context = context;
+ courseService = new(context);
+ this.rapsContext = rapsContext;
+ }
+
+ [HttpGet]
+ public async Task>> GetCourseList(string? termCode = null, string? subjectCode = null, string? courseNum = null, int? courseId = null)
+ {
+ if (termCode == null && courseId == null)
+ {
+ return BadRequest("Term code or course ID is required.");
+ }
+ var courses = await GetCourses(termCode, subjectCode, courseNum, courseId);
+ return mapper.Map>(courses);
+ }
+
+ [HttpGet("{courseId}")]
+ public async Task> GetCourse(int courseId)
+ {
+ var c = (await GetCourses(courseId: courseId)).FirstOrDefault();
+ if (c == null)
+ {
+ return NotFound();
+ }
+ return c;
+ }
+
+ /*
+ * Course Roles
+ */
+ [HttpGet("{courseId}/roles")]
+ public async Task>> GetCourseRoles(int courseId)
+ {
+ var courseCheck = await GetCourseForUser(courseId);
+ if (courseCheck == null)
+ {
+ return NotFound();
+ }
+
+ var roles = await context.CourseRoles
+ .Where(cr => cr.CourseId == courseId)
+ .Include(cr => cr.Role)
+ .Select(cr => cr.Role)
+ .OrderBy(r => r.Name)
+ .ToListAsync();
+ return mapper.Map>(roles);
+ }
+
+ [HttpPut("{courseId}/roles")]
+ [Permission(Allow = "SVMSecure.CTS.Manage")]
+ public async Task>> SetCourseRoles(int courseId, List roleIds)
+ {
+ var existingRoles = await context.CourseRoles
+ .Where(cr => cr.CourseId == courseId)
+ .ToListAsync();
+ var toRemove = existingRoles.Where(r => !roleIds.Contains(r.RoleId)).ToList();
+ var toAdd = roleIds.Where(r => !existingRoles.Any(er => er.RoleId == r)).ToList();
+
+ try
+ {
+ using var trans = context.Database.BeginTransaction();
+ foreach (var r in toRemove)
+ {
+ context.Remove(r);
+ }
+ foreach (var r in toAdd)
+ {
+ context.Add(new CourseRole()
+ {
+ CourseId = courseId,
+ RoleId = r
+ });
+ }
+ await context.SaveChangesAsync();
+ await trans.CommitAsync();
+ }
+ catch (Exception)
+ {
+ return BadRequest("Could not set roles.");
+ }
+
+ var rolesUpdated = await context.CourseRoles
+ .Where(cr => cr.CourseId == courseId)
+ .Include(cr => cr.Role)
+ .Select(cr => cr.Role)
+ .OrderBy(r => r.Name)
+ .ToListAsync();
+ return mapper.Map>(rolesUpdated);
+ }
+
+ /*
+ * Sessions
+ */
+ [HttpGet("{courseId}/sessions")]
+ public async Task>> GetCourseSessions(int courseId, bool? supportedSessionTypes = null, bool includeLegacyCompetencies = false)
+ {
+ var courseCheck = await GetCourseForUser(courseId);
+ if (courseCheck == null)
+ {
+ return NotFound();
+ }
+
+ var sessionsQ = context.Sessions
+ .Include(s => s.Competencies)
+ .Where(s => s.CourseId == courseId);
+ if (supportedSessionTypes ?? false)
+ {
+ sessionsQ = sessionsQ.Where(s => s.Type != null && CompetencySupportedSessionTypes.Contains(s.Type));
+ }
+ var sessions = await sessionsQ
+ .OrderBy(c => c.PaceOrder)
+ .ThenBy(c => c.SessionId)
+ .ToListAsync();
+ return mapper.Map>(sessions);
+ }
+
+ [HttpGet("{courseId}/sessions/{sessionId}")]
+ public async Task> GetSession(int courseId, int sessionId)
+ {
+ var courseCheck = await GetCourseForUser(courseId);
+ if (courseCheck == null)
+ {
+ return NotFound();
+ }
+
+ var session = await context.Sessions.FindAsync(sessionId);
+ if (session == null || session.CourseId != courseId)
+ {
+ return NotFound();
+ }
+ return mapper.Map(session);
+ }
+
+ /*
+ * Session Competencies
+ */
+ [HttpGet("{courseId}/sessions/{sessionId}/competencies")]
+ public async Task>> GetSessionCompetencies(int courseId, int sessionId)
+ {
+ var courseCheck = await GetCourseForUser(courseId);
+ if (courseCheck == null)
+ {
+ return NotFound();
+ }
+
+ var session = await GetCourseSession(courseId, sessionId);
+ if (session == null)
+ {
+ return NotFound();
+ }
+ var sessionComps = await context.SessionCompetencies
+ .Where(sc => sc.SessionId == sessionId)
+ .Include(sc => sc.Competency)
+ .Include(sc => sc.Level)
+ .Include(sc => sc.Role)
+ .OrderBy(sc => sc.Order)
+ .ThenBy(sc => sc.Competency.Number)
+ .ThenBy(sc => sc.CompetencyId)
+ .ThenBy(sc => sc.RoleId)
+ .ToListAsync();
+
+ List scs = new();
+ int lastComp = 0;
+ int? lastRole = 0;
+ SessionCompetencyDto? current = null;
+ foreach(var sessionCompetency in sessionComps)
+ {
+ if (lastComp != sessionCompetency.CompetencyId || lastRole != sessionCompetency.RoleId)
+ {
+ current = mapper.Map(sessionCompetency);
+ scs.Add(current);
+ lastComp = sessionCompetency.CompetencyId;
+ lastRole = sessionCompetency.RoleId;
+ }
+ if(current != null)
+ {
+ current.Levels.Add(new LevelIdAndNameDto()
+ {
+ LevelId = sessionCompetency.LevelId,
+ LevelName = sessionCompetency.Level.LevelName,
+ });
+ }
+ }
+
+ return scs;
+ }
+
+ [HttpPost("{courseId}/sessions/{sessionId}/competencies")]
+ [Permission(Allow = "SVMSecure.CTS.Manage")]
+ public async Task>> AddSessionCompetency(int courseId, int sessionId, CreateUpdateSessionCompetency sessionComp)
+ {
+ var checkResult = await CheckAddUpdateSessionComp(courseId, sessionId, sessionComp);
+ if (checkResult != null)
+ {
+ return checkResult;
+ }
+
+ foreach (var levelId in sessionComp.LevelIds)
+ {
+ var newSessionComp = new SessionCompetency()
+ {
+ CompetencyId = sessionComp.CompetencyId,
+ SessionId = sessionComp.SessionId,
+ LevelId = levelId,
+ RoleId = sessionComp.RoleId,
+ Order = sessionComp.Order ?? 0
+ };
+ context.Add(newSessionComp);
+ }
+ await context.SaveChangesAsync();
+
+ var sessionComps = await context.SessionCompetencies
+ .Where(sc => sc.SessionId == sessionComp.SessionId)
+ .Where(sc => sc.CompetencyId == sessionComp.CompetencyId)
+ .ToListAsync();
+
+ return mapper.Map>(sessionComps);
+ }
+
+ [HttpPut("{courseId}/sessions/{sessionId}/competencies")]
+ [Permission(Allow = "SVMSecure.CTS.Manage")]
+ public async Task>> UpdateSessionCompetency(int courseId, int sessionId, CreateUpdateSessionCompetency sessionComp)
+ {
+ var checkResult = await CheckAddUpdateSessionComp(courseId, sessionId, sessionComp);
+ if (checkResult != null)
+ {
+ return checkResult;
+ }
+
+ var existing = await context.SessionCompetencies
+ .Where(sc => sc.SessionId == sessionComp.SessionId)
+ .Where(sc => sc.CompetencyId == sessionComp.CompetencyId)
+ .ToListAsync();
+ var toRemove = existing.Where(e => !sessionComp.LevelIds.Contains(e.LevelId)).ToList();
+ var toAdd = sessionComp.LevelIds.Where(l => !existing.Any(esc => esc.LevelId == l)).ToList();
+
+ try
+ {
+ using var trans = context.Database.BeginTransaction();
+ foreach (var r in toRemove)
+ {
+ context.Remove(r);
+ }
+ foreach (var l in toAdd)
+ {
+ var newSessionComp = new SessionCompetency()
+ {
+ CompetencyId = sessionComp.CompetencyId,
+ SessionId = sessionComp.SessionId,
+ LevelId = l,
+ RoleId = sessionComp.RoleId,
+ Order = sessionComp.Order ?? 0
+ };
+ context.Add(newSessionComp);
+ }
+ await context.SaveChangesAsync();
+ await trans.CommitAsync();
+ }
+ catch (Exception)
+ {
+ return BadRequest("Could not update levels.");
+ }
+
+ existing = await context.SessionCompetencies
+ .Where(sc => sc.SessionId == sessionComp.SessionId)
+ .Where(sc => sc.CompetencyId == sessionComp.CompetencyId)
+ .ToListAsync();
+ return mapper.Map>(existing);
+ }
+
+ [HttpDelete("{courseId}/sessions/{sessionId}/competencies/{competencyId}")]
+ [Permission(Allow = "SVMSecure.CTS.Manage")]
+ public async Task DeleteSessionCompetency(int courseId, int sessionId, int competencyId, int? roleId = null)
+ {
+ var comps = await context.SessionCompetencies
+ .Where(sc => sc.SessionId == sessionId)
+ .Where(sc => sc.CompetencyId == competencyId)
+ .Where(rc => rc.RoleId == roleId)
+ .ToListAsync();
+
+ if(comps.Count == 0)
+ {
+ return NotFound();
+ }
+
+ foreach(var sc in comps)
+ {
+ context.Remove(sc);
+ }
+
+ await context.SaveChangesAsync();
+ return NoContent();
+ }
+
+ ///
+ /// Check arguments for adding or updating a session competency
+ ///
+ ///
+ ///
+ ///
+ ///
+ private async Task CheckAddUpdateSessionComp(int courseId, int sessionId, CreateUpdateSessionCompetency sessionComp)
+ {
+ if (sessionComp.SessionId != sessionId)
+ {
+ return BadRequest();
+ }
+
+ var courseCheck = await GetCourseForUser(courseId);
+ if (courseCheck == null)
+ {
+ return NotFound();
+ }
+
+ var session = await GetCourseSession(courseId, sessionId);
+ if (session == null)
+ {
+ return NotFound();
+ }
+
+ if (sessionComp.LevelIds == null || sessionComp.LevelIds.Count == 0)
+ {
+ return BadRequest("At least one level is required.");
+ }
+
+ if (sessionComp.RoleId != null)
+ {
+ //check that session is multi role and the role belongs to this course
+ if (!(session.MultiRole ?? false))
+ {
+ return BadRequest("Session is not multi-role.");
+ }
+
+ var courseRoles = await context.CourseRoles.Where(cr => cr.CourseId == courseId).Select(cr => cr.RoleId).ToListAsync();
+ if (!courseRoles.Contains((int)sessionComp.RoleId))
+ {
+ return BadRequest("Invalid Role.");
+ }
+ }
+
+ if (sessionComp.SessionCompetencyId == null)
+ {
+ var existing = await context.SessionCompetencies
+ .Where(sc => sc.SessionId == sessionId)
+ .Where(sc => sc.CompetencyId == sessionComp.CompetencyId)
+ .Where(rc => rc.RoleId == sessionComp.RoleId)
+ .FirstOrDefaultAsync();
+ if (existing != null)
+ {
+ return BadRequest("Competency is already attached to session. Please edit existing record instead.");
+ }
+ }
+ return null;
+ }
+
+ ///
+ /// Check that a course exists and the user has access to it.
+ ///
+ ///
+ ///
+ private async Task GetCourseForUser(int courseId)
+ {
+ var c = await context.Courses.FindAsync(courseId);
+ if (c == null)
+ {
+ return null;
+ }
+
+ var courseIds = await GetCourseIdsForUserForTerm(c.AcademicYear);
+ if (!courseIds.Contains(c.CourseId))
+ {
+ return null;
+ }
+
+ return c;
+ }
+
+ private async Task GetCourseSession(int courseId, int sessionId)
+ {
+ var session = await context.Sessions.FindAsync(sessionId);
+ return (session != null && session.CourseId == courseId) ? session : null;
+ }
+
+ private async Task> GetCourses(string? termCode = null, string? subjectCode = null, string? courseNum = null, int? courseId = null)
+ {
+ //get list of all courses matching criteria
+ var courses = context.Courses.AsQueryable();
+ if (termCode != null)
+ {
+ courses = courses.Where(c => c.AcademicYear == termCode);
+ }
+ if (subjectCode != null)
+ {
+ courses = courses.Where(c => c.CourseNum.StartsWith(subjectCode));
+ }
+ if (courseNum != null)
+ {
+ courses = courses.Where(c => c.CourseNum.EndsWith(courseNum));
+ }
+ if (courseId != null)
+ {
+ courses = courses.Where(c => c.CourseId == courseId);
+ }
+ var courseList = await courses.OrderBy(c => c.AcademicYear)
+ .ThenBy(c => c.CourseNum)
+ .ToListAsync();
+
+ //get allowed courses per term
+ var allTerms = courseList.Select(c => c.AcademicYear).ToList().Distinct();
+ List validCourseIds = new List();
+ foreach (var t in allTerms)
+ {
+ var courseIdsThisTerm = await GetCourseIdsForUserForTerm(t);
+ validCourseIds.AddRange(courseIdsThisTerm);
+ }
+ courseList = courseList.Where(c => validCourseIds.Contains(c.CourseId)).ToList();
+
+ //var courses = await courseService.GetCourses(termCode: termCode, subjectCode: subjectCode, courseNum: courseNum);
+ var courseDtos = mapper.Map>(courseList);
+
+ foreach (var c in courseDtos)
+ {
+ c.CompetencyCount = await context.SessionCompetencies
+ .Include(sc => sc.Session)
+ .Where(sc => sc.Session.CourseId == c.CourseId)
+ .Select(sc => sc.CompetencyId)
+ .Distinct()
+ .CountAsync();
+ }
+
+ return courseDtos;
+ }
+
+ ///
+ /// Return a list of course ids that the logged in user can access
+ /// Faculty can view courses they are the leader of or an instructor for
+ /// Dept proxies can view their courses
+ /// CTS.Manage and CTS.LoginStudents can view all courses
+ /// Students cannot view courses
+ ///
+ ///
+ ///
+ private async Task> GetCourseIdsForUserForTerm(string termCode)
+ {
+ var userHelper = new UserHelper();
+ if (userHelper.HasPermission(rapsContext, userHelper.GetCurrentUser(), "SVMSecure.CTS.Student"))
+ {
+ return new List();
+ }
+
+ var isAdmin = userHelper.HasPermission(rapsContext, userHelper.GetCurrentUser(), "SVMSecure.CTS.Manage")
+ || userHelper.HasPermission(rapsContext, userHelper.GetCurrentUser(), "SVMSecure.CTS.LoginStudents");
+ if (isAdmin)
+ {
+ return await context.Courses
+ .Where(c => c.AcademicYear == termCode)
+ .Select(c => c.CourseId)
+ .ToListAsync();
+ }
+
+ return context.GetMyCourses(termCode, Int32.Parse(userHelper?.GetCurrentUser()?.Pidm ?? "0"))
+ .Select(c => c.CourseId)
+ .ToList();
+ }
+ }
}
diff --git a/web/Areas/CTS/Controllers/DomainController.cs b/web/Areas/CTS/Controllers/DomainController.cs
index 3a73493..eb5bc8f 100644
--- a/web/Areas/CTS/Controllers/DomainController.cs
+++ b/web/Areas/CTS/Controllers/DomainController.cs
@@ -2,6 +2,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Linq.Dynamic.Core;
+using Viper.Areas.CTS.Models;
using Viper.Classes;
using Viper.Classes.SQLContext;
using Viper.Models.CTS;
@@ -10,7 +11,7 @@
namespace Viper.Areas.CTS.Controllers
{
[Route("/api/cts/domains")]
- [Permission(Allow = "SVMSecure")]
+ [Permission(Allow = "SVMSecure.CTS")]
public class DomainController : ApiController
{
private readonly VIPERContext context;
@@ -21,20 +22,22 @@ public DomainController(VIPERContext context)
}
[HttpGet]
- public async Task>> Index()
+ public async Task>> Index()
{
- return await context.Domains.OrderBy(d => d.Order).ToListAsync();
+ return await context.Domains.OrderBy(d => d.Order).Select(d => new DomainDto(d)).ToListAsync();
}
[HttpGet("{domainId}")]
- public async Task> GetDomain(int domainId)
+ public async Task> GetDomain(int domainId)
{
- return await context.Domains.FindAsync(domainId);
+ var d = await context.Domains.FindAsync(domainId);
+
+ return d == null ? NotFound() : new DomainDto(d);
}
[HttpPost]
[Permission(Allow = "SVMSecure.CTS.Manage")]
- public async Task> CreateDomain(Domain domain)
+ public async Task> CreateDomain(Domain domain)
{
if(domain.DomainId != 0)
{
@@ -51,12 +54,12 @@ public async Task> CreateDomain(Domain domain)
context.Add(domain);
await context.SaveChangesAsync();
- return domain;
+ return new DomainDto(domain);
}
[HttpPut("{domainId}")]
[Permission(Allow = "SVMSecure.CTS.Manage")]
- public async Task> UpdateDomain(int domainId, Domain domain)
+ public async Task> UpdateDomain(int domainId, Domain domain)
{
if(domain.DomainId != domainId)
{
@@ -72,12 +75,12 @@ public async Task> UpdateDomain(int domainId, Domain domain
context.Domains.Update(domain);
await context.SaveChangesAsync();
- return domain;
- }
+ return new DomainDto(domain);
+ }
[HttpDelete("{domainId}")]
[Permission(Allow = "SVMSecure.CTS.Manage")]
- public async Task> DeleteDomain(int domainId)
+ public async Task> DeleteDomain(int domainId)
{
var domain = await context.Domains.FindAsync(domainId);
if(domain == null)
@@ -94,7 +97,7 @@ public async Task> DeleteDomain(int domainId)
{
return BadRequest("Could not remove domain. It may be linked to other objects.");
}
- return domain;
+ return new DomainDto(domain);
}
}
}
diff --git a/web/Areas/CTS/Controllers/EncounterController.cs b/web/Areas/CTS/Controllers/EncounterController.cs
index e45ad07..a2c526a 100644
--- a/web/Areas/CTS/Controllers/EncounterController.cs
+++ b/web/Areas/CTS/Controllers/EncounterController.cs
@@ -7,7 +7,7 @@
namespace Viper.Areas.CTS.Controllers
{
[Route("/api/cts/encounters")]
- [Permission(Allow = "SVMSecure")]
+ [Permission(Allow = "SVMSecure.CTS")]
public class EncounterController : ApiController
{
private readonly VIPERContext context;
diff --git a/web/Areas/CTS/Controllers/EpaController.cs b/web/Areas/CTS/Controllers/EpaController.cs
index ce8794d..4aabec6 100644
--- a/web/Areas/CTS/Controllers/EpaController.cs
+++ b/web/Areas/CTS/Controllers/EpaController.cs
@@ -1,8 +1,6 @@
-using AngleSharp.Dom;
-using Ganss.Xss;
+using Ganss.Xss;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
-using Polly;
using Viper.Classes;
using Viper.Classes.SQLContext;
using Viper.Models.CTS;
@@ -11,7 +9,7 @@
namespace Viper.Areas.CTS.Controllers
{
[Route("/api/cts/epas")]
- [Permission(Allow = "SVMSecure")]
+ [Permission(Allow = "SVMSecure.CTS")]
public class EpaController : ApiController
{
private readonly VIPERContext context;
@@ -116,6 +114,7 @@ public async Task> DeleteEpa(int epaId)
}
[HttpPut("{epaId}/services")]
+ [Permission(Allow = "SVMSecure.CTS.Manage")]
public async Task UpdateServices(int epaId, List serviceIds)
{
var epa = await context.Epas
diff --git a/web/Areas/CTS/Controllers/LegacyCompetenciesController.cs b/web/Areas/CTS/Controllers/LegacyCompetenciesController.cs
new file mode 100644
index 0000000..91a2e34
--- /dev/null
+++ b/web/Areas/CTS/Controllers/LegacyCompetenciesController.cs
@@ -0,0 +1,102 @@
+using AutoMapper;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Viper.Areas.CTS.Models;
+using Viper.Classes;
+using Viper.Classes.SQLContext;
+using Viper.Models.CTS;
+using Web.Authorization;
+
+namespace Viper.Areas.CTS.Controllers
+{
+ [Route("/api/cts/legacyCompetencies/")]
+ [Permission(Allow = "SVMSecure.CTS.Manage")]
+ public class LegacyCompetenciesController : ApiController
+ {
+ private readonly VIPERContext context;
+ private readonly IMapper mapper;
+
+ public LegacyCompetenciesController(VIPERContext context, IMapper mapper)
+ {
+ this.context = context;
+ this.mapper = mapper;
+ }
+
+ [HttpGet]
+ public async Task>> Get()
+ {
+ var legacyComps = await context.LegacyCompetencies
+ .Include(e => e.DvmCompetencyMapping)
+ .ThenInclude(e => e.Competency)
+ .ToListAsync();
+
+ return mapper.Map>(legacyComps);
+ }
+
+ [HttpGet("session/{sessionId}")]
+ public async Task>> GetSessionCompentencies(int sessionId)
+ {
+ var comps = await context.LegacySessionCompetencies
+ .Where(l => l.SessionId == sessionId)
+ .OrderBy(l => l.SessionCompetencyOrder)
+ .ThenBy(l => l.DvmCompetencyId)
+ .ThenBy(l => l.DvmRoleName)
+ .ThenBy(l => l.DvmLevelOrder)
+ .ToListAsync();
+ return GroupSessionCompetencies(comps);
+ }
+
+ [HttpGet("course/{courseId}")]
+ public async Task>> GetCourseCompetencies(int courseId)
+ {
+ var comps = await context.LegacySessionCompetencies
+ .Where(l => l.CourseId == courseId)
+ .OrderBy(l => l.PaceOrder)
+ .ThenBy(l => l.SessionCompetencyOrder)
+ .ThenBy(l => l.DvmCompetencyId)
+ .ThenBy(l => l.DvmRoleName)
+ .ThenBy(l => l.DvmLevelOrder)
+ .ToListAsync();
+ return GroupSessionCompetencies(comps);
+ }
+
+ private List GroupSessionCompetencies(List comps)
+ {
+ List lscs = new();
+ int lastComp = 0;
+ int? lastRole = null;
+ LegacySessionCompetencyDto? current = null;
+ foreach (var sessionCompetency in comps)
+ {
+ if (sessionCompetency.DvmCompetencyId != null && lastComp != sessionCompetency.DvmCompetencyId || lastRole != sessionCompetency.DvmRoleId)
+ {
+ current = mapper.Map(sessionCompetency);
+ lscs.Add(current);
+ lastComp = (int)sessionCompetency.DvmCompetencyId!;
+ lastRole = sessionCompetency.DvmRoleId;
+ }
+ if (current != null && sessionCompetency.DvmLevelId != null)
+ {
+ current.Levels.Add(new LevelIdAndNameDto()
+ {
+ LevelId = (int)sessionCompetency.DvmLevelId,
+ LevelName = sessionCompetency.DvmLevelName,
+ });
+ }
+ }
+ return lscs;
+ }
+
+ [HttpGet("term/{termCode}")]
+ public async Task>> GetCoursesCompetenciesForTerm(int termCode)
+ {
+ var comps = await context.LegacySessionCompetencies
+ .Where(l => l.AcademicYear == termCode.ToString())
+ .OrderBy(l => l.CourseTitle)
+ .ThenBy(l => l.PaceOrder)
+ .ThenBy(l => l.SessionCompetencyOrder)
+ .ToListAsync();
+ return mapper.Map>(comps);
+ }
+ }
+}
diff --git a/web/Areas/CTS/Controllers/LevelsController.cs b/web/Areas/CTS/Controllers/LevelsController.cs
index c73aec1..09bd5cf 100644
--- a/web/Areas/CTS/Controllers/LevelsController.cs
+++ b/web/Areas/CTS/Controllers/LevelsController.cs
@@ -12,7 +12,7 @@
namespace Viper.Areas.CTS.Controllers
{
[Route("/api/cts/levels")]
- [Permission(Allow = "SVMSecure")]
+ [Permission(Allow = "SVMSecure.CTS")]
public class LevelsController : ApiController
{
private readonly VIPERContext context;
@@ -22,13 +22,30 @@ public LevelsController(VIPERContext context)
}
[HttpGet]
- public async Task>> GetLevels(bool? epa = null, bool active = true)
+ public async Task>> GetLevels(bool? epa = null, bool? dops = null, bool? milestone = null,
+ bool? course = null, bool? clinical = null, bool active = true)
{
var q = context.Levels.AsQueryable();
if (epa != null)
{
q = q.Where(l => l.Epa == epa);
}
+ if (dops != null)
+ {
+ q = q.Where(l => l.Dops == dops);
+ }
+ if (milestone != null)
+ {
+ q = q.Where(l => l.Milestone == milestone);
+ }
+ if (course != null)
+ {
+ q = q.Where(l => l.Course == course);
+ }
+ if (clinical != null)
+ {
+ q = q.Where(l => l.Clinical == clinical);
+ }
q = q.Where(l => (active && l.Active) || (!active && !l.Active));
return await q.OrderBy(l => l.Epa ? 1 : 0)
.ThenBy(l => l.Dops ? 1 : 0)
@@ -61,6 +78,10 @@ public async Task> Createlevel(LevelCreateUpdate level)
{
return BadRequest("Another level for the same type of assessment has the same text.");
}
+ if (!level.Dops && !level.Clinical && !level.Course && !level.Milestone && !level.Epa)
+ {
+ return BadRequest("Level must belong to a type.");
+ }
var l = new Level()
{
@@ -153,6 +174,22 @@ private void AdjustLevelOrders(Level level)
{
levels = levels.Where(l => l.Epa);
}
+ if (level.Milestone)
+ {
+ levels = levels.Where(l => l.Milestone);
+ }
+ if (level.Course)
+ {
+ levels = levels.Where(l => l.Course);
+ }
+ if (level.Clinical)
+ {
+ levels = levels.Where(l => l.Clinical);
+ }
+ if (level.Dops)
+ {
+ levels = levels.Where(l => l.Dops);
+ }
levels = levels.OrderBy(l => l.Order);
//check orders are correct
diff --git a/web/Areas/CTS/Controllers/MilestonesController.cs b/web/Areas/CTS/Controllers/MilestonesController.cs
new file mode 100644
index 0000000..3f7fad2
--- /dev/null
+++ b/web/Areas/CTS/Controllers/MilestonesController.cs
@@ -0,0 +1,113 @@
+using AutoMapper;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Viper.Areas.CTS.Models;
+using Viper.Classes;
+using Viper.Classes.SQLContext;
+using Web.Authorization;
+
+namespace Viper.Areas.CTS.Controllers
+{
+ [Route("/api/cts/milestones/")]
+ [Permission(Allow = "SVMSecure.CTS")]
+ public class MilestonesController : ApiController
+ {
+ private readonly VIPERContext context;
+ private readonly IMapper mapper;
+
+ public MilestonesController(VIPERContext context, IMapper mapper)
+ {
+ this.context = context;
+ this.mapper = mapper;
+ }
+
+ //NB: Milestones are defined as bundles, so add/update/delete functions are in BundlesController
+ //The GetMilestones function returns a simplified and more Milestone-centric object
+
+ [HttpGet]
+ public async Task>> GetMilestones()
+ {
+ var bundleQuery = await context.Bundles
+ .Include(b => b.BundleCompetencies)
+ .ThenInclude(b => b.Competency)
+ .Where(b => b.Milestone)
+ .OrderBy(b => b.Name)
+ .ToListAsync();
+ return mapper.Map>(bundleQuery);
+ }
+
+
+ [HttpGet("{milestoneId}/levels")]
+ public async Task>> GetMilestoneLevels(int milestoneId)
+ {
+ var bundleExists = await context.Bundles
+ .Where(b => b.BundleId == milestoneId)
+ .Where(b => b.Milestone)
+ .AnyAsync();
+ if (!bundleExists)
+ {
+ return NotFound();
+ }
+ var levelQuery = await context.MilestoneLevels
+ .Include(ml => ml.Level)
+ .Where(ml => ml.BundleId == milestoneId)
+ .OrderBy(ml => ml.Level.Order)
+ .ToListAsync();
+ return mapper.Map>(levelQuery);
+ }
+
+ [HttpPut("{milestoneId}/levels")]
+ [Permission(Allow = "SVMSecure.CTS.Manage")]
+ public async Task>> SetMilestoneLevels(int milestoneId, List milestoneLevels)
+ {
+ var bundleExists = await context.Bundles
+ .Where(b => b.BundleId == milestoneId)
+ .Where(b => b.Milestone)
+ .AnyAsync();
+ if (!bundleExists)
+ {
+ return NotFound();
+ }
+ var existingLevels = await context.MilestoneLevels
+ .Where(ml => ml.BundleId == milestoneId)
+ .ToListAsync();
+
+ foreach (var ml in milestoneLevels)
+ {
+ //look for existing to determine whether to add milestoneLevel or update description
+ var existingLevel = existingLevels.Where(el => el.LevelId == ml.LevelId).FirstOrDefault();
+ if (existingLevel != null)
+ {
+ existingLevel.Description = ml.Description;
+ context.MilestoneLevels.Update(existingLevel);
+ }
+ else
+ {
+ context.MilestoneLevels.Add(new Viper.Models.CTS.MilestoneLevel()
+ {
+ BundleId = milestoneId,
+ LevelId = ml.LevelId,
+ Description = ml.Description,
+ });
+ }
+ }
+
+ //shouldn't happen often, but if a level was not sent in the milestoneLevels argument, delete the milestone level
+ //e.g. if the level is no longer in use
+ var levelIds = milestoneLevels.Select(ml => ml.LevelId).ToList();
+ var toDelete = existingLevels.Where(el => !levelIds.Contains(el.LevelId)).ToList();
+ foreach(var deleteLevel in toDelete)
+ {
+ context.MilestoneLevels.Remove(deleteLevel);
+ }
+
+ await context.SaveChangesAsync();
+
+ var savedLevels = await context.MilestoneLevels
+ .Include(ml => ml.Level)
+ .Where(ml => ml.BundleId == milestoneId)
+ .ToListAsync();
+ return mapper.Map>(savedLevels);
+ }
+ }
+}
diff --git a/web/Areas/CTS/Controllers/PermissionsController.cs b/web/Areas/CTS/Controllers/PermissionsController.cs
new file mode 100644
index 0000000..82b61ee
--- /dev/null
+++ b/web/Areas/CTS/Controllers/PermissionsController.cs
@@ -0,0 +1,39 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Viper.Areas.CTS.Services;
+using Viper.Classes;
+using Viper.Classes.SQLContext;
+using Web.Authorization;
+
+namespace Viper.Areas.CTS.Controllers
+{
+ [Route("/api/cts/permissions/")]
+ [Permission(Allow = "SVMSecure.CTS")]
+ public class PermissionsController : ApiController
+ {
+ private readonly CtsSecurityService ctsSecurityService;
+ private readonly RAPSContext _rapsContext;
+ private readonly VIPERContext _viperContext;
+
+ public PermissionsController(VIPERContext context, RAPSContext rapsContext)
+ {
+ _viperContext = context;
+ _rapsContext = rapsContext;
+ ctsSecurityService = new CtsSecurityService(rapsContext, _viperContext);
+ }
+
+ [HttpGet]
+ public ActionResult HasAccess(string access, int studentId)
+ {
+ var userHelper = new UserHelper();
+ switch(access)
+ {
+ case "ViewStudentAssessments":
+ return ctsSecurityService.CheckStudentAssessmentViewAccess(studentId);
+ case "ViewAllAssessments":
+ return ctsSecurityService.CheckStudentAssessmentViewAccess();
+ }
+ return false;
+ }
+ }
+}
diff --git a/web/Areas/CTS/Controllers/RoleController.cs b/web/Areas/CTS/Controllers/RoleController.cs
new file mode 100644
index 0000000..f258da7
--- /dev/null
+++ b/web/Areas/CTS/Controllers/RoleController.cs
@@ -0,0 +1,99 @@
+using AutoMapper;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using System.Diagnostics;
+using Viper.Areas.CTS.Models;
+using Viper.Classes;
+using Viper.Classes.SQLContext;
+using Viper.Models.CTS;
+using Web.Authorization;
+
+namespace Viper.Areas.CTS.Controllers
+{
+ [Route("/api/cts/roles")]
+ [Permission(Allow = "SVMSecure.CTS")]
+ public class RoleController : ApiController
+ {
+ private readonly VIPERContext context;
+ private readonly IMapper mapper;
+
+ public RoleController(VIPERContext context, IMapper mapper)
+ {
+ this.context = context;
+ this.mapper = mapper;
+ }
+
+ public async Task>> GetRoles(int? bundleId = null)
+ {
+ var rolesQuery = context.Roles.AsQueryable();
+ if (bundleId != null)
+ {
+ rolesQuery = rolesQuery.Where(r => r.BundleRoles.Any(br => br.BundleId == bundleId));
+ }
+ var roles = await rolesQuery.OrderBy(r => r.RoleId).ToListAsync();
+ return mapper.Map>(roles);
+ }
+
+ [HttpPost]
+ [Permission(Allow = "SVMSecure.CTS.Manage")]
+ public async Task> AddRole(RoleDto roleDto)
+ {
+ var checkName = await context.Roles.AnyAsync(r => r.Name == roleDto.Name);
+ if (checkName)
+ {
+ return BadRequest("Role name must be unique.");
+ }
+
+ var role = mapper.Map(roleDto);
+ context.Add(role);
+ await context.SaveChangesAsync();
+
+ return mapper.Map(role);
+ }
+
+ [HttpPut("{roleId}")]
+ [Permission(Allow = "SVMSecure.CTS.Manage")]
+ public async Task> UpdateRole(int roleId, RoleDto roleDto)
+ {
+ var role = await context.Roles.FindAsync(roleId);
+ if(role == null)
+ {
+ return NotFound();
+ }
+
+ var checkName = await context.Roles.AnyAsync(r => r.Name == roleDto.Name && r.RoleId != roleId);
+ if (checkName)
+ {
+ return BadRequest("Role name must be unique.");
+ }
+
+ role.Name = roleDto.Name;
+ context.Update(role);
+ await context.SaveChangesAsync();
+ return mapper.Map(role);
+ }
+
+ [HttpDelete("{roleId}")]
+ [Permission(Allow = "SVMSecure.CTS.Manage")]
+ public async Task> DeleteRole(int roleId)
+ {
+ var role = await context.Roles.FindAsync(roleId);
+ if (role == null)
+ {
+ return NotFound();
+ }
+
+ try
+ {
+ context.Entry(role).State = EntityState.Deleted;
+ await context.SaveChangesAsync();
+ }
+ catch (Exception)
+ {
+ return BadRequest("Could not delete role. If this role has been added to a bundle, it cannot be deleted.");
+ }
+
+ return mapper.Map(role);
+ }
+ }
+}
diff --git a/web/Areas/CTS/Models/AutoMapperProfileCts.cs b/web/Areas/CTS/Models/AutoMapperProfileCts.cs
new file mode 100644
index 0000000..5fad1fb
--- /dev/null
+++ b/web/Areas/CTS/Models/AutoMapperProfileCts.cs
@@ -0,0 +1,74 @@
+using AutoMapper;
+using Viper.Models.CTS;
+
+namespace Viper.Areas.CTS.Models
+{
+ public class AutoMapperProfileCts : Profile
+ {
+ public AutoMapperProfileCts() {
+ CreateMap()
+ .ForMember(dest => dest.Levels, opt => opt.MapFrom(src => src.BundleCompetencyLevels.Select(bcl => bcl.Level).ToList()))
+ .ForMember(dest => dest.CompetencyName, opt => opt.MapFrom(src => src.Competency.Name))
+ .ForMember(dest => dest.CompetencyNumber, opt => opt.MapFrom(src => src.Competency.Number))
+ .ForMember(dest => dest.RoleName, opt => opt.MapFrom(src => src.Role == null ? null : src.Role.Name))
+ .ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Competency.Description))
+ .ForMember(dest => dest.CanLinkToStudent, opt => opt.MapFrom(src => src.Competency.CanLinkToStudent));
+ CreateMap();
+ CreateMap()
+ .ForMember(dest => dest.CompetencyCount, opt => opt.MapFrom(src => src.BundleCompetencies.Count))
+ .ForMember(dest => dest.Roles, opt => opt.MapFrom(src => src.BundleRoles.Select(br => br.Role).ToList()))
+ .ReverseMap();
+ CreateMap();
+ CreateMap()
+ .ForMember(dest => dest.DomainName, opt => opt.MapFrom(src => src.Domain.Name))
+ .ForMember(dest => dest.DomainOrder, opt => opt.MapFrom(src => src.Domain.Order));
+ CreateMap().ReverseMap();
+ CreateMap();
+ CreateMap();
+ CreateMap()
+ .ForMember(dest => dest.MilestoneId, opt => opt.MapFrom(src => src.BundleId))
+ .ForMember(
+ dest => dest.CompetencyName,
+ opt => opt.MapFrom(src =>
+ src.BundleCompetencies.Count > 0
+ ? src.BundleCompetencies.First().Competency.Name
+ : null
+ ))
+ .ForMember(
+ dest => dest.CompetencyId,
+ opt => opt.MapFrom(src =>
+ src.BundleCompetencies.Count > 0
+ ? src.BundleCompetencies.First().Competency.CompetencyId
+ : (int?)null
+ ));
+ CreateMap()
+ .ForMember(dest => dest.MilestoneId, opt => opt.MapFrom(src => src.BundleId))
+ .ForMember(dest => dest.LevelName, opt => opt.MapFrom(src => src.Level.LevelName))
+ .ForMember(dest => dest.LevelOrder, opt => opt.MapFrom(src => src.Level.Order));
+
+ //course, session
+ CreateMap();
+ CreateMap()
+ .ForMember(dest => dest.CompetencyCount, opt => opt.MapFrom(src => src.Competencies.Select(c => c.CompetencyId).Distinct().Count()));
+ CreateMap()
+ /*
+ .ForMember(dest => dest.SessionName, opt => opt.MapFrom(src => src.Session.Title))
+ .ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.Session.Type))
+ .ForMember(dest => dest.TypeOrder, opt => opt.MapFrom(src => src.Session.TypeOrder))
+ .ForMember(dest => dest.PaceOrder, opt => opt.MapFrom(src => src.Session.PaceOrder))
+ .ForMember(dest => dest.MultiRole, opt => opt.MapFrom(src => src.Session.MultiRole))
+ */
+ .ForMember(dest => dest.CompetencyName, opt => opt.MapFrom(src => src.Competency.Name))
+ .ForMember(dest => dest.CompetencyNumber, opt => opt.MapFrom(src => src.Competency.Number))
+ .ForMember(dest => dest.CanLinkToStudent, opt => opt.MapFrom(src => src.Competency.CanLinkToStudent))
+ .ForMember(dest => dest.RoleName, opt => opt.MapFrom(src => src.Role != null ? src.Role.Name : null));
+
+ //Legacy comps
+ CreateMap()
+ .ForMember(dest => dest.Competencies, opt => opt.MapFrom(src =>
+ src.DvmCompetencyMapping.Select(d => d.Competency).ToList()
+ ));
+ CreateMap();
+ }
+ }
+}
diff --git a/web/Areas/CTS/Models/BundleCompetencyAddUpdate.cs b/web/Areas/CTS/Models/BundleCompetencyAddUpdate.cs
new file mode 100644
index 0000000..237ac67
--- /dev/null
+++ b/web/Areas/CTS/Models/BundleCompetencyAddUpdate.cs
@@ -0,0 +1,13 @@
+namespace Viper.Areas.CTS.Models
+{
+ public class BundleCompetencyAddUpdate
+ {
+ public int? BundleCompetencyId { get; set; }
+ public int BundleId { get; set; }
+ public int CompetencyId { get; set; }
+ public int Order { get; set; }
+ public List LevelIds { get; set; } = new List();
+ public int? RoleId { get; set; }
+ public int? BundleCompetencyGroupId { get; set; }
+ }
+}
diff --git a/web/Areas/CTS/Models/BundleCompetencyDto.cs b/web/Areas/CTS/Models/BundleCompetencyDto.cs
new file mode 100644
index 0000000..97c377d
--- /dev/null
+++ b/web/Areas/CTS/Models/BundleCompetencyDto.cs
@@ -0,0 +1,24 @@
+namespace Viper.Areas.CTS.Models
+{
+ public class BundleCompetencyDto
+ {
+ public int BundleCompetencyId { get; set; }
+ public int BundleId { get; set; }
+
+ public int? RoleId { get; set; }
+ public string? RoleName { get; set; } = null!;
+
+ public IEnumerable Levels { get; set; } = new List();
+
+ //comp info
+ public int CompetencyId { get; set; }
+ public string CompetencyNumber { get; set; } = null!;
+ public string CompetencyName { get; set; } = null!;
+ public string? Description { get; set; }
+ public bool CanLinkToStudent { get; set; }
+
+ //group and order
+ public int? BundleCompetencyGroupId { get; set; }
+ public int Order { get; set; }
+ }
+}
diff --git a/web/Areas/CTS/Models/BundleCompetencyGroupDto.cs b/web/Areas/CTS/Models/BundleCompetencyGroupDto.cs
new file mode 100644
index 0000000..2831033
--- /dev/null
+++ b/web/Areas/CTS/Models/BundleCompetencyGroupDto.cs
@@ -0,0 +1,10 @@
+namespace Viper.Areas.CTS.Models
+{
+ public class BundleCompetencyGroupDto
+ {
+ public int? BundleCompetencyGroupId { get; set; }
+ public string Name { get; set; } = null!;
+ public int Order { get; set; }
+ //public IEnumerable Competencies { get; set; } = new List();
+ }
+}
diff --git a/web/Areas/CTS/Models/BundleDto.cs b/web/Areas/CTS/Models/BundleDto.cs
new file mode 100644
index 0000000..81314bb
--- /dev/null
+++ b/web/Areas/CTS/Models/BundleDto.cs
@@ -0,0 +1,17 @@
+using Viper.Models.CTS;
+
+namespace Viper.Areas.CTS.Models
+{
+ public class BundleDto
+ {
+ public int? BundleId { get; set; }
+ public string Name { get; set; } = null!;
+ public bool Clinical { get; set; }
+ public bool Assessment { get; set; }
+ public bool Milestone { get; set; }
+ public int CompetencyCount { get; set; }
+
+ //Only including roles here - other related objects can be found separately
+ public IEnumerable Roles{ get; set; } = new List();
+ }
+}
diff --git a/web/Areas/CTS/Models/BundleRoleDto.cs b/web/Areas/CTS/Models/BundleRoleDto.cs
new file mode 100644
index 0000000..0838876
--- /dev/null
+++ b/web/Areas/CTS/Models/BundleRoleDto.cs
@@ -0,0 +1,9 @@
+namespace Viper.Areas.CTS.Models
+{
+ public class BundleRoleDto
+ {
+ public int BundleRoleId { get; set; }
+ public int BundleId { get; set; }
+ public int RoleId { get; set; }
+ }
+}
diff --git a/web/Areas/CTS/Models/CompetencyAddUpdate.cs b/web/Areas/CTS/Models/CompetencyAddUpdate.cs
new file mode 100644
index 0000000..858161d
--- /dev/null
+++ b/web/Areas/CTS/Models/CompetencyAddUpdate.cs
@@ -0,0 +1,13 @@
+namespace Viper.Areas.CTS.Models
+{
+ public class CompetencyAddUpdate
+ {
+ public int? CompetencyId { get; set; }
+ public int DomainId { get; set; }
+ public int? ParentId { get; set; }
+ public string Number { get; set; } = null!;
+ public string Name { get; set; } = null!;
+ public string? Description { get; set; }
+ public bool CanLinkToStudent { get; set; }
+ }
+}
diff --git a/web/Areas/CTS/Models/CompetencyDto.cs b/web/Areas/CTS/Models/CompetencyDto.cs
new file mode 100644
index 0000000..7e7e40c
--- /dev/null
+++ b/web/Areas/CTS/Models/CompetencyDto.cs
@@ -0,0 +1,37 @@
+using Viper.Models.CTS;
+
+namespace Viper.Areas.CTS.Models
+{
+ public class CompetencyDto
+ {
+ public int CompetencyId { get; set; }
+ public int DomainId { get; set; }
+ public int? ParentId { get; set; }
+ public string Number { get; set; } = null!;
+ public string Name { get; set; } = null!;
+ public string? Description { get; set; }
+ public bool CanLinkToStudent { get; set; }
+ public string? DomainName { get; set; }
+ public int? DomainOrder { get; set; }
+ public CompetencyDto? Parent { get; set; }
+
+ public CompetencyDto()
+ {
+
+ }
+
+ public CompetencyDto(Competency c)
+ {
+ CompetencyId = c.CompetencyId;
+ DomainId = c.DomainId;
+ ParentId = c.ParentId;
+ Number = c.Number;
+ Name = c.Name;
+ Description = c.Description;
+ CanLinkToStudent = c.CanLinkToStudent;
+ DomainName = c?.Domain?.Name;
+ DomainOrder = c?.Domain?.Order;
+ Parent = c?.Parent != null ? new CompetencyDto(c.Parent) : null;
+ }
+ }
+}
diff --git a/web/Areas/CTS/Models/CompetencyHierarchyDto.cs b/web/Areas/CTS/Models/CompetencyHierarchyDto.cs
new file mode 100644
index 0000000..c44ff60
--- /dev/null
+++ b/web/Areas/CTS/Models/CompetencyHierarchyDto.cs
@@ -0,0 +1,29 @@
+namespace Viper.Areas.CTS.Models
+{
+ public class CompetencyHierarchyDto
+ {
+ public int CompetencyId { get; set; }
+ public int DomainId { get; set; }
+ public int? ParentId { get; set; }
+ public string Number { get; set; } = null!;
+ public string Name { get; set; } = null!;
+ public string? Description { get; set; }
+ public bool CanLinkToStudent { get; set; }
+ public string? DomainName { get; set; }
+ public int? DomainOrder { get; set; }
+ public IEnumerable Children { get; set; } = new List();
+ public string Type
+ {
+ get
+ {
+ return Number.Split(".", StringSplitOptions.RemoveEmptyEntries).Length switch
+ {
+ 2 => "Competency",
+ 3 => "Illustrative Sub-Competency",
+ 4 => "Detail",
+ _ => "Unknown",
+ };
+ }
+ }
+ }
+}
diff --git a/web/Areas/CTS/Models/CourseDto.cs b/web/Areas/CTS/Models/CourseDto.cs
new file mode 100644
index 0000000..21ffa89
--- /dev/null
+++ b/web/Areas/CTS/Models/CourseDto.cs
@@ -0,0 +1,17 @@
+namespace Viper.Areas.CTS.Models
+{
+ public class CourseDto
+ {
+ public int CourseId { get; set; }
+ public string Status { get; set; } = null!;
+ public string Title { get; set; } = null!;
+ public string? Description { get; set; }
+ public string AcademicYear { get; set; } = null!;
+ public string? Crn { get; set; }
+ public string CourseNum { get; set; } = null!;
+
+ public int? CompetencyCount { get; set; }
+
+ //public List Sessions { get; set; } = new List();
+ }
+}
diff --git a/web/Areas/CTS/Models/CreateUpdateSessionCompetency.cs b/web/Areas/CTS/Models/CreateUpdateSessionCompetency.cs
new file mode 100644
index 0000000..a3205eb
--- /dev/null
+++ b/web/Areas/CTS/Models/CreateUpdateSessionCompetency.cs
@@ -0,0 +1,17 @@
+namespace Viper.Areas.CTS.Models
+{
+ public class CreateUpdateSessionCompetency
+ {
+ public int? SessionCompetencyId { get; set; }
+
+ public int SessionId { get; set; }
+
+ public int CompetencyId { get; set; }
+
+ public List LevelIds { get; set; } = new List();
+
+ public int? RoleId { get; set; }
+
+ public int? Order { get; set; }
+ }
+}
diff --git a/web/Areas/CTS/Models/DomainDto.cs b/web/Areas/CTS/Models/DomainDto.cs
new file mode 100644
index 0000000..418017a
--- /dev/null
+++ b/web/Areas/CTS/Models/DomainDto.cs
@@ -0,0 +1,21 @@
+using Viper.Models.CTS;
+
+namespace Viper.Areas.CTS.Models
+{
+ public class DomainDto
+ {
+ public int DomainId { get; set; }
+ public string Name { get; set; } = null!;
+ public int Order { get; set; }
+ public string? Description { get; set; }
+
+ public DomainDto() { }
+ public DomainDto(Domain domain)
+ {
+ DomainId = domain.DomainId;
+ Name = domain.Name;
+ Order = domain.Order;
+ Description = domain.Description;
+ }
+ }
+}
diff --git a/web/Areas/CTS/Models/EvaluateeWithEpaCompletion.cs b/web/Areas/CTS/Models/EvaluateeWithEpaCompletion.cs
new file mode 100644
index 0000000..3d8500f
--- /dev/null
+++ b/web/Areas/CTS/Models/EvaluateeWithEpaCompletion.cs
@@ -0,0 +1,47 @@
+using Viper.Models.CTS;
+
+namespace Viper.Areas.CTS.Models
+{
+ public class EvaluateeWithEpaCompletion
+ {
+ public int EvaluateeId { get; set; }
+ public int EvalId { get; set; }
+ public int RotId { get; set; }
+ public string Rotation { get; set; } = null!;
+ public int ServiceId { get; set; }
+ public string Service { get; set; } = null!;
+ public int StartWeekId { get; set; }
+ public DateTime StartDate { get; set; }
+ public int EndWeekId { get; set; }
+ public DateTime EndDate { get; set; }
+ public string InstructorMothraId { get; set; } = null!;
+ public int InstanceId { get; set; }
+ public string FirstName { get; set; } = null!;
+ public string LastName { get; set; } = null!;
+ public string MothraId { get; set; } = null!;
+ public int PersonId { get; set; }
+ public bool EpaDone { get; set; }
+
+ public EvaluateeWithEpaCompletion() { }
+ public EvaluateeWithEpaCompletion(EvaluateesByInstance e, bool epaDone = false)
+ {
+ EvaluateeId = e.EvaluateeId;
+ EvalId = e.EvalId;
+ RotId = e.RotId;
+ Rotation = e.Rotation;
+ ServiceId = e.ServiceId;
+ Service = e.Service;
+ StartWeekId = e.StartWeekId;
+ StartDate = e.StartDate;
+ EndWeekId = e.EndWeekId;
+ EndDate = e.EndDate;
+ InstructorMothraId = e.InstructorMothraId;
+ InstanceId = e.InstanceId;
+ FirstName = e.FirstName;
+ LastName = e.LastName;
+ MothraId = e.MothraId;
+ PersonId = e.PersonId;
+ EpaDone = epaDone;
+ }
+ }
+}
diff --git a/web/Areas/CTS/Models/LegacyCompetencyDto.cs b/web/Areas/CTS/Models/LegacyCompetencyDto.cs
new file mode 100644
index 0000000..0e0538c
--- /dev/null
+++ b/web/Areas/CTS/Models/LegacyCompetencyDto.cs
@@ -0,0 +1,12 @@
+namespace Viper.Areas.CTS.Models
+{
+ public class LegacyCompetencyDto
+ {
+ public int DvmCompetencyId { get; set; }
+ public string DvmCompetencyName { get; set; } = null!;
+ public int? DvmCompetencyParentId { get; set; }
+ public bool DvmCompetencyActive { get; set; }
+
+ public List Competencies { get; set; } = new List();
+ }
+}
\ No newline at end of file
diff --git a/web/Areas/CTS/Models/LegacySessionCompetencyDto.cs b/web/Areas/CTS/Models/LegacySessionCompetencyDto.cs
new file mode 100644
index 0000000..7875c70
--- /dev/null
+++ b/web/Areas/CTS/Models/LegacySessionCompetencyDto.cs
@@ -0,0 +1,28 @@
+namespace Viper.Areas.CTS.Models
+{
+ public class LegacySessionCompetencyDto
+ {
+ //SessionCompetency
+ public int SessionCompetencyId { get; set; }
+ public int? SessionCompetencyOrder { get; set; }
+
+ //Session
+ public int SessionId { get; set; }
+
+ //Course
+ public int CourseId { get; set; }
+
+ //Competency
+ public int? DvmCompetencyId { get; set; }
+ public string? DvmCompetencyName { get; set; }
+ public int? DvmCompetencyParentId { get; set; }
+ public bool? DvmCompetencyActive { get; set; }
+
+ //Levels
+ public List Levels { get; set; } = new List();
+
+ //Role
+ public int? DvmRoleId { get; set; }
+ public string? DvmRoleName { get; set; }
+ }
+}
diff --git a/web/Areas/CTS/Models/LevelDto.cs b/web/Areas/CTS/Models/LevelDto.cs
new file mode 100644
index 0000000..8ee196d
--- /dev/null
+++ b/web/Areas/CTS/Models/LevelDto.cs
@@ -0,0 +1,16 @@
+namespace Viper.Areas.CTS.Models
+{
+ public class LevelDto
+ {
+ public int LevelId { get; set; }
+ public string Level { get; set; } = null!;
+ public string? Description { get; set; } = null!;
+ public bool Active { get; set; }
+ public int Order { get; set; }
+ public bool Course { get; set; }
+ public bool Clinical { get; set; }
+ public bool Assessment { get; set; }
+ public bool Milestone { get; set; }
+ public bool Epa { get; set; }
+ }
+}
diff --git a/web/Areas/CTS/Models/LevelIdAndNameDto.cs b/web/Areas/CTS/Models/LevelIdAndNameDto.cs
new file mode 100644
index 0000000..666bec8
--- /dev/null
+++ b/web/Areas/CTS/Models/LevelIdAndNameDto.cs
@@ -0,0 +1,8 @@
+namespace Viper.Areas.CTS.Models
+{
+ public class LevelIdAndNameDto
+ {
+ public int LevelId { get; set; }
+ public string LevelName { get; set; } = null!;
+ }
+}
diff --git a/web/Areas/CTS/Models/MilestoneDto.cs b/web/Areas/CTS/Models/MilestoneDto.cs
new file mode 100644
index 0000000..7462933
--- /dev/null
+++ b/web/Areas/CTS/Models/MilestoneDto.cs
@@ -0,0 +1,10 @@
+namespace Viper.Areas.CTS.Models
+{
+ public class MilestoneDto
+ {
+ public int MilestoneId { get; set; }
+ public string Name { get; set; } = null!;
+ public int? CompetencyId { get; set; }
+ public string? CompetencyName { get; set; }
+ }
+}
diff --git a/web/Areas/CTS/Models/MilestoneLevelAddUpdate.cs b/web/Areas/CTS/Models/MilestoneLevelAddUpdate.cs
new file mode 100644
index 0000000..fd1ce75
--- /dev/null
+++ b/web/Areas/CTS/Models/MilestoneLevelAddUpdate.cs
@@ -0,0 +1,8 @@
+namespace Viper.Areas.CTS.Models
+{
+ public class MilestoneLevelAddUpdate
+ {
+ public int LevelId { get; set; }
+ public string Description { get; set; } = null!;
+ }
+}
diff --git a/web/Areas/CTS/Models/MilestoneLevelDto.cs b/web/Areas/CTS/Models/MilestoneLevelDto.cs
new file mode 100644
index 0000000..2d3de67
--- /dev/null
+++ b/web/Areas/CTS/Models/MilestoneLevelDto.cs
@@ -0,0 +1,12 @@
+namespace Viper.Areas.CTS.Models
+{
+ public class MilestoneLevelDto
+ {
+ public int? MilestoneLevelId { get; set; }
+ public int MilestoneId { get; set; }
+ public int LevelId { get; set; }
+ public string LevelName { get; set; } = null!;
+ public int LevelOrder { get; set; }
+ public string Description { get; set; } = null!;
+ }
+}
\ No newline at end of file
diff --git a/web/Areas/CTS/Models/RoleDto.cs b/web/Areas/CTS/Models/RoleDto.cs
new file mode 100644
index 0000000..506f120
--- /dev/null
+++ b/web/Areas/CTS/Models/RoleDto.cs
@@ -0,0 +1,8 @@
+namespace Viper.Areas.CTS.Models
+{
+ public class RoleDto
+ {
+ public int RoleId { get; set; }
+ public string Name { get; set; } = null!;
+ }
+}
diff --git a/web/Areas/CTS/Models/ServiceDto.cs b/web/Areas/CTS/Models/ServiceDto.cs
new file mode 100644
index 0000000..0da048a
--- /dev/null
+++ b/web/Areas/CTS/Models/ServiceDto.cs
@@ -0,0 +1,8 @@
+namespace Viper.Areas.CTS.Models
+{
+ public class ServiceDto
+ {
+ public int ServiceId { get; set; }
+ public string Name { get; set; } = null!;
+ }
+}
\ No newline at end of file
diff --git a/web/Areas/CTS/Models/SessionCompetencyDto.cs b/web/Areas/CTS/Models/SessionCompetencyDto.cs
new file mode 100644
index 0000000..f084694
--- /dev/null
+++ b/web/Areas/CTS/Models/SessionCompetencyDto.cs
@@ -0,0 +1,33 @@
+using Viper.Models.CTS;
+
+namespace Viper.Areas.CTS.Models
+{
+ public class SessionCompetencyDto
+ {
+ public int SessionCompetencyId { get; set; }
+ public int Order { get; set; }
+
+ //session info
+ //public int SessionId { get; set; }
+ //public string SessionName { get; set; } = string.Empty;
+ //public string? Type { get; set; }
+ //public string? TypeDescription { get; set; }
+ //public string Title { get; set; } = null!;
+ //public string CourseTitle { get; set; } = null!;
+ //public string? Description { get; set; }
+ //public int TypeOrder { get; set; }
+ //public int PaceOrder { get; set; }
+ //public bool? MultiRole { get; set; }
+
+ //comp info
+ public int CompetencyId { get; set; }
+ public string CompetencyNumber { get; set; } = string.Empty;
+ public string CompetencyName { get; set; } = string.Empty;
+ public bool CanLinkToStudent { get; set; }
+
+ //level and role
+ public List Levels { get; set; } = new List();
+ public int? RoleId { get; set; }
+ public string? RoleName { get; set; } = string.Empty;
+ }
+}
diff --git a/web/Areas/CTS/Models/SessionDto.cs b/web/Areas/CTS/Models/SessionDto.cs
new file mode 100644
index 0000000..fbcda15
--- /dev/null
+++ b/web/Areas/CTS/Models/SessionDto.cs
@@ -0,0 +1,22 @@
+namespace Viper.Areas.CTS.Models
+{
+ public class SessionDto
+ {
+ public int SessionId { get; set; }
+ public string Status { get; set; } = null!;
+ public string? Type { get; set; }
+ public string? TypeDescription { get; set; }
+ public string Title { get; set; } = null!;
+ public string CourseTitle { get; set; } = null!;
+ public string? Description { get; set; }
+ public int CourseId { get; set; }
+ public string AcademicYear { get; set; } = null!;
+ public int TypeOrder { get; set; }
+ public int PaceOrder { get; set; }
+ public bool? MultiRole { get; set; }
+
+ public int CompetencyCount { get; set; }
+ public int LegacyComptencyCount { get; set; }
+ //public List Competencies { get; set; } = new List();
+ }
+}
diff --git a/web/Areas/CTS/Services/CrestCourseService.cs b/web/Areas/CTS/Services/CrestCourseService.cs
index c00d5b0..e6abdcf 100644
--- a/web/Areas/CTS/Services/CrestCourseService.cs
+++ b/web/Areas/CTS/Services/CrestCourseService.cs
@@ -1,12 +1,13 @@
using Microsoft.EntityFrameworkCore;
-using NuGet.Protocol;
using Viper.Areas.CTS.Models;
using Viper.Classes.SQLContext;
using Viper.Models.CTS;
+using Course = Viper.Areas.CTS.Models.Course;
+using Session = Viper.Areas.CTS.Models.Session;
namespace Viper.Areas.CTS.Services
{
- public class CrestCourseService
+ public class CrestCourseService
{
private readonly VIPERContext _context;
public CrestCourseService(VIPERContext context)
diff --git a/web/Areas/CTS/Services/CtsNavMenu.cs b/web/Areas/CTS/Services/CtsNavMenu.cs
index 84f9670..4852113 100644
--- a/web/Areas/CTS/Services/CtsNavMenu.cs
+++ b/web/Areas/CTS/Services/CtsNavMenu.cs
@@ -36,20 +36,27 @@ public NavMenu Nav()
//Assessments of the logged in user
if (userHelper.HasPermission(_rapsContext, userHelper.GetCurrentUser(), "SVMSecure.CTS.Students"))
{
- nav.Add(new NavMenuItem() { MenuItemText = "My Assessments", MenuItemURL = "MyAssessments" });
+ //nav.Add(new NavMenuItem() { MenuItemText = "My Assessments", MenuItemURL = "MyAssessments" });
}
if (userHelper.HasPermission(_rapsContext, userHelper.GetCurrentUser(), "SVMSecure.CTS.Manage"))
{
+ nav.Add(new NavMenuItem() { MenuItemText = "Courses", IsHeader = true });
+ nav.Add(new NavMenuItem() { MenuItemText = "Course Competencies", MenuItemURL = "ManageCourseCompetencies" });
+ nav.Add(new NavMenuItem() { MenuItemText = "Legacy Competency Mapping", MenuItemURL = "ManageLegacyCompetencyMapping" });
+
nav.Add(new NavMenuItem() { MenuItemText = "Admin Functions", IsHeader = true });
nav.Add(new NavMenuItem() { MenuItemText = "Manage Domains", MenuItemURL = "ManageDomains" });
nav.Add(new NavMenuItem() { MenuItemText = "Manage Competencies", MenuItemURL = "ManageCompetencies" });
+ nav.Add(new NavMenuItem() { MenuItemText = "Manage Bundles", MenuItemURL = "ManageBundles" });
nav.Add(new NavMenuItem() { MenuItemText = "Manage Levels", MenuItemURL = "ManageLevels" });
nav.Add(new NavMenuItem() { MenuItemText = "Manage EPAs", MenuItemURL = "ManageEPAs" });
+ nav.Add(new NavMenuItem() { MenuItemText = "Manage Milestones", MenuItemURL = "ManageMilestones" });
+ nav.Add(new NavMenuItem() { MenuItemText = "Manage Roles", MenuItemURL = "ManageRoles" });
nav.Add(new NavMenuItem() { MenuItemText = "Audit Log", MenuItemURL = "Audit" });
- nav.Add(new NavMenuItem() { MenuItemText = "Reports", IsHeader = true });
- nav.Add(new NavMenuItem() { MenuItemText = "Assessment Charts", MenuItemURL = "AssessmentChart" });
+ //nav.Add(new NavMenuItem() { MenuItemText = "Reports", IsHeader = true });
+ //nav.Add(new NavMenuItem() { MenuItemText = "Assessment Charts", MenuItemURL = "AssessmentChart" });
}
return new NavMenu("Competency Tracking System", nav);
diff --git a/web/Areas/CTS/Services/CtsSecurityService.cs b/web/Areas/CTS/Services/CtsSecurityService.cs
index 54803f9..440e88a 100644
--- a/web/Areas/CTS/Services/CtsSecurityService.cs
+++ b/web/Areas/CTS/Services/CtsSecurityService.cs
@@ -37,7 +37,7 @@ public bool CheckStudentAssessmentViewAccess(int? studentId = null, int? entered
return true;
}
if (userHelper.HasPermission(rapsContext, userHelper.GetCurrentUser(), AssessClinicalPermission)
- && enteredBy == userHelper.GetCurrentUser()?.AaudUserId)
+ && enteredBy != null && enteredBy == userHelper.GetCurrentUser()?.AaudUserId)
{
return true;
}
diff --git a/web/Areas/Computing/Controllers/PersonController.cs b/web/Areas/Computing/Controllers/PersonController.cs
index c5ed57b..575e2a5 100644
--- a/web/Areas/Computing/Controllers/PersonController.cs
+++ b/web/Areas/Computing/Controllers/PersonController.cs
@@ -43,6 +43,17 @@ public async Task>> GetPeople(bool? active = nul
.ToListAsync();
}
+ [HttpGet("{personId}")]
+ public async Task> GetPerson(int personId)
+ {
+ var p = await context.People.FindAsync(personId);
+ if(p == null)
+ {
+ return NotFound();
+ }
+ return new PersonSimple(p);
+ }
+
[HttpPost("biorenderStudents")]
[Permission(Allow = "SVMSecure.CATS.BiorenderStudentLookup")]
public async Task>> GetBiorenderStudentList(List emails)
diff --git a/web/Areas/Curriculum/Controllers/TermsController.cs b/web/Areas/Curriculum/Controllers/TermsController.cs
index ad02e08..a366746 100644
--- a/web/Areas/Curriculum/Controllers/TermsController.cs
+++ b/web/Areas/Curriculum/Controllers/TermsController.cs
@@ -9,6 +9,7 @@
namespace Viper.Areas.Curriculum.Controllers
{
[Route("/curriculum/terms")]
+ [Route("/api/curriculum/terms")]
[Permission(Allow = "SVMSecure.Curriculum")]
public class TermsController : ApiController
{
diff --git a/web/Areas/Students/Controller/DvmController.cs b/web/Areas/Students/Controller/DvmController.cs
index 04c42f0..19334a5 100644
--- a/web/Areas/Students/Controller/DvmController.cs
+++ b/web/Areas/Students/Controller/DvmController.cs
@@ -47,7 +47,7 @@ public DvmStudentsController(VIPERContext context, RAPSContext rapsContext)
{
includeAllClassYears = false;
}
- var students = await studentList.GetStudents(classLevel: classLevel, classYear: classYear, activeYearOnly: !includeAllClassYears);
+ var students = await studentList.GetStudents(classLevel: classLevel, classYear: classYear, activeYearOnly: !includeAllClassYears, currentYearsOnly: false);
return students;
}
diff --git a/web/Areas/Students/Services/StudentList.cs b/web/Areas/Students/Services/StudentList.cs
index 29af66c..ee1121f 100644
--- a/web/Areas/Students/Services/StudentList.cs
+++ b/web/Areas/Students/Services/StudentList.cs
@@ -111,8 +111,8 @@ public async Task> GetStudentsByTermCodeAndClassLevel(int termCode
{
//Get students based on AAUD Student info for the given term
var students = _context.People
- .Include(p => p.StudentInfo)
- .Where(p => p.StudentInfo != null && p.StudentInfo.TermCode == termCode && p.StudentInfo.ClassLevel == classLevel)
+ .Include(p => p.StudentHistory)
+ .Where(p => p.StudentHistory != null && p.StudentHistory.Any(h => h.TermCode == termCode && h.ClassLevel == classLevel))
.OrderBy(p => p.LastName)
.ThenBy(p => p.FirstName);
var studentList = await students
diff --git a/web/Classes/ApiController.cs b/web/Classes/ApiController.cs
index cd333e8..d7aa2cd 100644
--- a/web/Classes/ApiController.cs
+++ b/web/Classes/ApiController.cs
@@ -54,5 +54,10 @@ public IQueryable GetPage(IQueryable query, ApiPagination? pagination)
}
return query;
}
+
+ protected ActionResult ForbidApi(string message = "Access Denied.")
+ {
+ return StatusCode(StatusCodes.Status403Forbidden, message);
+ }
}
}
diff --git a/web/Classes/SQLContext/CtsContext.cs b/web/Classes/SQLContext/CtsContext.cs
index 176a5b8..26377c6 100644
--- a/web/Classes/SQLContext/CtsContext.cs
+++ b/web/Classes/SQLContext/CtsContext.cs
@@ -1,4 +1,5 @@
-using Microsoft.EntityFrameworkCore;
+using Microsoft.Data.SqlClient;
+using Microsoft.EntityFrameworkCore;
using Viper.Models.CTS;
namespace Viper.Classes.SQLContext;
@@ -14,6 +15,21 @@ public partial class VIPERContext : DbContext
public virtual DbSet Encounters { get; set; }
public virtual DbSet EncounterInstructors { get; set; }
public virtual DbSet CtsAudits { get; set; }
+ public virtual DbSet Roles { get; set; }
+ public virtual DbSet Bundles { get; set; }
+ public virtual DbSet BundleCompetencies { get; set; }
+ public virtual DbSet BundleCompetencyGroups { get; set; }
+ public virtual DbSet BundleCompetencyLevels { get; set; }
+ public virtual DbSet BundleRoles { get; set; }
+ public virtual DbSet BundleServices { get; set; }
+ public virtual DbSet CompetencyOutcomes { get; set; }
+ public virtual DbSet CourseCompetencies { get; set; }
+ public virtual DbSet CourseRoles { get; set; }
+ public virtual DbSet MilestoneLevels { get; set; }
+ public virtual DbSet Patients { get; set; }
+ public virtual DbSet SessionCompetencies { get; set; }
+ public virtual DbSet StudentCompetencies { get; set; }
+ public virtual DbSet CompetencyMappings { get; set; }
/* Students */
public virtual DbSet DvmStudent { get; set; }
@@ -28,6 +44,17 @@ public partial class VIPERContext : DbContext
/* CREST */
public virtual DbSet CourseSessionOffering { get; set; }
+ public virtual DbSet Courses { get; set; }
+ public virtual DbSet Sessions { get; set; }
+ public virtual DbSet MyCourses { get; set; }
+
+ /* Eval */
+ public virtual DbSet Instances { get; set; }
+ public virtual DbSet EvaluateesByInstances { get; set; }
+
+ /* Legacy CTS */
+ public virtual DbSet LegacyCompetencies { get; set; }
+ public virtual DbSet LegacySessionCompetencies { get; set; }
partial void OnModelCreatingCTS(ModelBuilder modelBuilder)
{
@@ -36,6 +63,9 @@ partial void OnModelCreatingCTS(ModelBuilder modelBuilder)
entity.ToTable("Competency", "cts");
entity.Property(e => e.Description).IsUnicode(false);
+ entity.Property(e => e.Number)
+ .HasMaxLength(20)
+ .IsUnicode(false);
entity.Property(e => e.Name)
.HasMaxLength(250)
.IsUnicode(false);
@@ -115,6 +145,249 @@ partial void OnModelCreatingCTS(ModelBuilder modelBuilder)
.HasForeignKey(e => e.EncounterId);
});
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("Role", "cts");
+
+ entity.Property(e => e.Name)
+ .HasMaxLength(250)
+ .IsUnicode(false);
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("Bundle", "cts");
+
+ entity.Property(e => e.Name)
+ .HasMaxLength(500)
+ .IsUnicode(false);
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("BundleCompetency", "cts");
+
+ entity.HasOne(d => d.BundleCompetencyGroup).WithMany(p => p.BundleCompetencies)
+ .HasForeignKey(d => d.BundleCompetencyGroupId)
+ .HasConstraintName("FK_BundleCompetency_BundleCompetencyGroupId");
+
+ entity.HasOne(d => d.Bundle).WithMany(p => p.BundleCompetencies)
+ .HasForeignKey(d => d.BundleId)
+ .OnDelete(DeleteBehavior.ClientSetNull)
+ .HasConstraintName("FK_BundleCompetency_Bundle");
+
+ entity.HasOne(d => d.Role).WithMany()
+ .HasForeignKey(d => d.RoleId)
+ .OnDelete(DeleteBehavior.ClientSetNull)
+ .HasConstraintName("FK_BundleCompetency_Role")
+ .IsRequired(false);
+
+ entity.HasOne(d => d.Competency).WithMany(p => p.BundleCompetencies)
+ .HasForeignKey(d => d.CompetencyId)
+ .OnDelete(DeleteBehavior.ClientSetNull)
+ .HasConstraintName("FK_BundleCompetency_Competency");
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.BundleCompetencyGroupId).HasName("PK_BundleCompetencyGroupId");
+
+ entity.ToTable("BundleCompetencyGroup", "cts");
+
+ entity.Property(e => e.Name)
+ .HasMaxLength(500)
+ .IsUnicode(false);
+
+ entity.HasOne(d => d.Bundle).WithMany(p => p.BundleCompetencyGroups)
+ .HasForeignKey(d => d.BundleId)
+ .OnDelete(DeleteBehavior.ClientSetNull)
+ .HasConstraintName("FK_BundleCompetencyGroupId_Bundle");
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("BundleCompetencyLevel", "cts");
+ entity.HasKey(e => e.BundleCompetencyLevelId);
+
+ entity.HasOne(d => d.BundleCompetency).WithMany(p => p.BundleCompetencyLevels)
+ .HasForeignKey(d => d.BundleCompetencyId)
+ .OnDelete(DeleteBehavior.ClientSetNull)
+ .HasConstraintName("FK_BundleLevel_BundleCompetency");
+ entity.HasOne(d => d.Level).WithMany()
+ .HasForeignKey(d => d.LevelId)
+ .OnDelete(DeleteBehavior.ClientSetNull)
+ .HasConstraintName("FK_BundleLevel_Level");
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("BundleRole", "cts");
+
+ entity.HasOne(d => d.Bundle).WithMany(p => p.BundleRoles)
+ .HasForeignKey(d => d.BundleId)
+ .OnDelete(DeleteBehavior.ClientSetNull)
+ .HasConstraintName("FK_BundleRole_Bundle");
+ entity.HasOne(d => d.Role).WithMany(r => r.BundleRoles)
+ .HasForeignKey(d => d.RoleId)
+ .OnDelete(DeleteBehavior.ClientSetNull)
+ .HasConstraintName("FK_BundleRole_Role");
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("BundleService", "cts");
+
+ entity.HasOne(d => d.Bundle).WithMany(p => p.BundleServices)
+ .HasForeignKey(d => d.BundleId)
+ .OnDelete(DeleteBehavior.ClientSetNull)
+ .HasConstraintName("FK_BundleService_Bundle");
+ entity.HasOne(d => d.Service).WithMany()
+ .HasForeignKey(d => d.ServiceId)
+ .OnDelete(DeleteBehavior.ClientSetNull);
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("CompetencyOutcome", "cts");
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("CourseCompetency", "cts");
+ entity.HasOne(d => d.Course).WithMany()
+ .HasForeignKey(d => d.CourseId)
+ .OnDelete(DeleteBehavior.ClientSetNull);
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("CourseRole", "cts");
+ entity.HasOne(d => d.Course).WithMany()
+ .HasForeignKey(d => d.CourseId)
+ .OnDelete(DeleteBehavior.ClientSetNull);
+ entity.HasOne(d => d.Role).WithMany()
+ .HasForeignKey(d => d.RoleId)
+ .OnDelete(DeleteBehavior.ClientSetNull);
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("MilestoneLevel", "cts");
+
+ entity.Property(e => e.Description).IsUnicode(false);
+
+ entity.HasOne(d => d.Bundle).WithMany(p => p.MilestoneLevels)
+ .HasForeignKey(d => d.BundleId)
+ .OnDelete(DeleteBehavior.ClientSetNull)
+ .HasConstraintName("FK_MilestoneLevel_Bundle");
+ entity.HasOne(d => d.Level).WithMany()
+ .HasForeignKey(d => d.LevelId)
+ .OnDelete(DeleteBehavior.ClientSetNull)
+ .HasConstraintName("FK_MilestoneLevel_MilestoneLevel");
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("Patient", "cts");
+
+ entity.Property(e => e.PatientId).ValueGeneratedNever();
+ entity.Property(e => e.Gender)
+ .HasMaxLength(100)
+ .IsUnicode(false);
+ entity.Property(e => e.PatientName)
+ .HasMaxLength(500)
+ .IsUnicode(false);
+ entity.Property(e => e.Species)
+ .HasMaxLength(100)
+ .IsUnicode(false);
+ entity.HasOne(d => d.Encounter).WithOne(d => d.Patient)
+ .HasForeignKey(d => d.PatientId)
+ .IsRequired(false);
+
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("SessionCompetency", "cts");
+ entity.HasKey(e => e.SessionCompetencyId);
+ entity.HasOne(d => d.Competency).WithMany()
+ .HasForeignKey(d => d.CompetencyId)
+ .OnDelete(DeleteBehavior.ClientSetNull)
+ .HasConstraintName("FK_SessionCompetency_Competency");
+ entity.HasOne(d => d.Session).WithMany(s => s.Competencies)
+ .HasForeignKey(d => d.SessionId)
+ .OnDelete(DeleteBehavior.ClientSetNull);
+ entity.HasOne(d => d.Level).WithMany()
+ .HasForeignKey(d => d.LevelId)
+ .OnDelete(DeleteBehavior.ClientSetNull)
+ .HasConstraintName("FK_SessionCompetency_Level");
+ entity.HasOne(d => d.Role).WithMany()
+ .HasForeignKey(d => d.RoleId)
+ .OnDelete(DeleteBehavior.ClientSetNull)
+ .HasConstraintName("FK_SessionCompetency_Role")
+ .IsRequired(false);
+
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("StudentCompetency", "cts");
+
+ entity.Property(e => e.Added).HasColumnType("datetime");
+ entity.Property(e => e.Updated).HasColumnType("datetime");
+ entity.Property(e => e.VerifiedTimestamp).HasColumnType("datetime");
+
+ entity.HasOne(d => d.BundleGroup).WithMany()
+ .HasForeignKey(d => d.BundleGroupId)
+ .HasConstraintName("FK_StudentCompetency_BundleCompetencyGroupId");
+
+ entity.HasOne(d => d.Bundle).WithMany()
+ .HasForeignKey(d => d.BundleId)
+ .HasConstraintName("FK_StudentCompetency_Bundle");
+
+ entity.HasOne(d => d.Person).WithMany()
+ .HasForeignKey(d => d.StudentUserId)
+ .HasConstraintName("FK_StudentCompetency_Student")
+ .OnDelete(DeleteBehavior.ClientSetNull);
+ entity.HasOne(d => d.Competency).WithMany()
+ .HasForeignKey(d => d.CompetencyId)
+ .HasConstraintName("FK_StudentCompetency_Competency")
+ .OnDelete(DeleteBehavior.ClientSetNull);
+ entity.HasOne(d => d.Level).WithMany()
+ .HasForeignKey(d => d.LevelId)
+ .HasConstraintName("FK_StudentCompetency_Level")
+ .OnDelete(DeleteBehavior.ClientSetNull);
+ entity.HasOne(d => d.Encounter).WithMany()
+ .HasForeignKey(d => d.EncounterId)
+ .HasConstraintName("FK_StudentCompetency_Encounter")
+ .OnDelete(DeleteBehavior.ClientSetNull)
+ .IsRequired(false);
+ entity.HasOne(d => d.Course).WithMany()
+ .HasForeignKey(d => d.CourseId)
+ .OnDelete(DeleteBehavior.ClientSetNull)
+ .IsRequired(false);
+ entity.HasOne(d => d.Session).WithMany()
+ .HasForeignKey(d => d.SessionId)
+ .OnDelete(DeleteBehavior.ClientSetNull)
+ .IsRequired(false);
+ entity.HasOne(d => d.Verifier).WithMany()
+ .HasForeignKey(d => d.VerifiedBy)
+ .HasConstraintName("FK_StudentCompetency_VerifiedPerson")
+ .OnDelete(DeleteBehavior.ClientSetNull)
+ .IsRequired(false);
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("CompetencyMapping", schema: "cts");
+ entity.HasKey(e => e.CompetencyMappingId);
+ entity.HasOne(e => e.Competency).WithMany()
+ .HasForeignKey(e => e.CompetencyId);
+ entity.HasOne(e => e.LegacyCompetency).WithMany(e => e.DvmCompetencyMapping)
+ .HasForeignKey(e => e.DvmCompetencyId);
+ });
+
+
/* "Exteral" entities */
modelBuilder.Entity(entity =>
{
@@ -235,5 +508,74 @@ partial void OnModelCreatingCTS(ModelBuilder modelBuilder)
entity.Property(e => e.CanvasCourseId).IsRequired(false);
entity.Property(e => e.CanvasEventId).IsRequired(false);
});
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("vwCourse", schema: "cts");
+ entity.HasKey(e => e.CourseId);
+ entity.Property(e => e.Description).IsRequired(false);
+ entity.Property(e => e.Crn).IsRequired(false);
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("vwSession", schema: "cts");
+ entity.HasKey(e => e.SessionId);
+ entity.Property(e => e.Type).IsRequired(false);
+ entity.Property(e => e.TypeDescription).IsRequired(false);
+ entity.Property(e => e.Description).IsRequired(false);
+ });
+
+ //Legacy CTS Competencies and CREST session competencies
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("vwLegacyCompetencies", schema: "cts");
+ entity.HasKey(e => e.DvmCompetencyId);
+ });
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("vwLegacySessionCompetencies", schema: "cts");
+ entity.HasKey(e => new
+ {
+ e.SessionCompetencyId,
+ e.SessionId
+ });
+ });
+
+ /*
+ modelBuilder.Entity(entity =>
+ {
+
+ });
+ */
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.InstanceId);
+ entity.ToTable("vwInstances", schema: "eval");
+ entity.Property(e => e.InstanceMode).IsRequired(false);
+ entity.Property(e => e.InstanceDueDate).IsRequired(false);
+ entity.Property(e => e.InstanceStartWeek).IsRequired(false);
+ entity.Property(e => e.InstanceStartWeekId).IsRequired(false);
+ });
+
+
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.EvaluateeId);
+ entity.ToTable("vwEvaluateesByInstances", schema: "eval");
+ });
}
+
+ /*
+ * Stored procedures
+ */
+ public virtual List GetMyCourses(string academicYear, int userPidm)
+ {
+ //var academicYearParam = new SqlParameter("@academicYear", academicYear);
+ //var userPidmParam = new SqlParameter("@userPidm", userPidm);
+ return MyCourses.FromSql($"getMyCourses {academicYear} {userPidm}").ToList();
+ }
+
}
diff --git a/web/Classes/SQLContext/VIPERContext.cs b/web/Classes/SQLContext/VIPERContext.cs
index 982097b..f8d65c5 100644
--- a/web/Classes/SQLContext/VIPERContext.cs
+++ b/web/Classes/SQLContext/VIPERContext.cs
@@ -1322,6 +1322,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
entity.HasKey("PersonId");
entity.HasOne(e => e.StudentInfo).WithMany()
.HasForeignKey(e => new { e.StudentTerm, e.SpridenId });
+ entity.HasMany(e => e.StudentHistory)
+ .WithOne()
+ .HasForeignKey(e => e.SpridenId)
+ .HasPrincipalKey(e => e.SpridenId);
});
OnModelCreatingCTS(modelBuilder);
OnModelCreatingStudents(modelBuilder);
diff --git a/web/Controllers/HomeController.cs b/web/Controllers/HomeController.cs
index f74a817..a5ad3b9 100644
--- a/web/Controllers/HomeController.cs
+++ b/web/Controllers/HomeController.cs
@@ -56,7 +56,7 @@ public IActionResult Index()
[Authorize(Policy = "2faAuthentication")]
[Permission(Allow = "SVMSecure")]
public IActionResult Policy()
- {
+ {
return View();
}
diff --git a/web/Models/CTS/Bundle.cs b/web/Models/CTS/Bundle.cs
new file mode 100644
index 0000000..82fd1c4
--- /dev/null
+++ b/web/Models/CTS/Bundle.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+
+namespace Viper.Models.CTS;
+
+public partial class Bundle
+{
+ public int BundleId { get; set; }
+
+ public string Name { get; set; } = null!;
+
+ public bool Clinical { get; set; }
+
+ public bool Assessment { get; set; }
+
+ public bool Milestone { get; set; }
+
+ public virtual ICollection BundleCompetencies { get; set; } = new List();
+
+ public virtual ICollection BundleCompetencyGroups { get; set; } = new List();
+
+ //public virtual ICollection BundleLevels { get; set; } = new List();
+
+ public virtual ICollection BundleRoles { get; set; } = new List();
+
+ public virtual ICollection BundleServices { get; set; } = new List