Skip to content
2 changes: 2 additions & 0 deletions packages/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1940,6 +1940,8 @@ export const InsightCategories = [
'Queues',
'Chatbot',
'Staff',
'Other_Sections',
'Other_Semesters',
]

export enum InsightType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const EditCourseForm: React.FC<EditCourseFormProps> = ({
const [formGeneral] = Form.useForm()
const [professors, setProfessors] = useState<OrganizationProfessor[]>()

// PAT TODO: add a popconfirm that tells the user that changing the name will detach the course from courses of the same name

const isAdmin =
user && user.organization?.organizationRole === OrganizationRole.ADMIN

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ const InsightsMenu: React.FC<InsightsMenuProps> = ({
case 'Staff':
icon = <TeamOutlined />
break
case 'Other_Sections':
icon = <LineChartOutlined /> //PAT TODO: replace with a better icon
break
case 'Other_Semesters':
icon = <LineChartOutlined /> //PAT TODO: replace with a better icon
break
}

return {
Expand Down
3 changes: 2 additions & 1 deletion packages/server/ormconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ import { LMSAssignmentModel } from './src/lmsIntegration/lmsAssignment.entity';
import { LMSAnnouncementModel } from './src/lmsIntegration/lmsAnnouncement.entity';
import { UnreadAsyncQuestionModel } from './src/asyncQuestion/unread-async-question.entity';
import { AsyncQuestionCommentModel } from './src/asyncQuestion/asyncQuestionComment.entity';
import { ChatbotDocPdfModel } from './src/chatbot/chatbot-doc-pdf.entity';
import { SuperCourseModel } from './src/course/super-course.entity';
import { ChatbotDocPdfModel } from 'chatbot/chatbot-doc-pdf.entity';
// set .envs to their default values if the developer hasn't yet set them
if (fs.existsSync('.env')) {
config();
Expand Down Expand Up @@ -106,6 +106,7 @@ const typeorm: DataSourceOptions = {
ApplicationConfigModel,
QueueInviteModel,
QueueChatsModel,
SuperCourseModel,
InsightDashboardModel,
LMSOrganizationIntegrationModel,
LMSCourseIntegrationModel,
Expand Down
14 changes: 7 additions & 7 deletions packages/server/src/course/course.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,17 +137,17 @@ export class CourseModel extends BaseEntity {
@Exclude()
unreadAsyncQuestions: UnreadAsyncQuestionModel[];

@OneToMany(
(type) => ChatbotDocPdfModel,
(chatbotDocPdf) => chatbotDocPdf.course,
)
@Exclude()
chatbot_doc_pdfs: ChatbotDocPdfModel[];

@ManyToOne(() => SuperCourseModel, (course) => course.courses)
@JoinColumn({ name: 'superCourseId' })
superCourse: SuperCourseModel;

@Column({ nullable: true })
superCourseId: number;

@OneToMany(
(type) => ChatbotDocPdfModel,
(chatbotDocPdf) => chatbotDocPdf.course,
)
@Exclude()
chatbot_doc_pdfs: ChatbotDocPdfModel[];
}
25 changes: 21 additions & 4 deletions packages/server/src/course/course.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { CourseSettingsModel } from './course_settings.entity';
import { OrganizationUserModel } from 'organization/organization-user.entity';
import { OrganizationCourseModel } from 'organization/organization-course.entity';
import { SemesterModel } from 'semester/semester.entity';
import { SuperCourseModel } from './super-course.entity';
import { MailService } from 'mail/mail.service';
import { ChatbotApiService } from 'chatbot/chatbot-api.service';
import { InternalServerErrorException } from '@nestjs/common';
Expand Down Expand Up @@ -551,6 +552,22 @@ export class CourseService {
'Either a new semester or new section must be provided for your course clone.',
);
}

// SuperCourses are used to group courses together for insights that span multiple semesters
// They are generated here based solely on the course's name for now
const standardizedCourseName = clonedCourse.name.trim().toLowerCase();
const superCourse = await manager.findOne(SuperCourseModel, {
where: { name: standardizedCourseName },
});
if (!superCourse) {
const newSuperCourse = manager.create(SuperCourseModel, {
name: standardizedCourseName,
organizationId: organizationUser.organizationId,
});
await manager.save(newSuperCourse);
}
clonedCourse.superCourse = superCourse;

await manager.save(clonedCourse);

if (originalCourse.courseSettings) {
Expand Down Expand Up @@ -594,10 +611,10 @@ export class CourseService {
await this.redisProfileService.deleteProfile(`u:${userId}`);
}

const organizationCourse = new OrganizationCourseModel();

organizationCourse.courseId = clonedCourse.id;
organizationCourse.organizationId = organizationUser.organizationId;
const organizationCourse = await manager.create(OrganizationCourseModel, {
courseId: clonedCourse.id,
organizationId: organizationUser.organizationId,
});
await manager.save(organizationCourse);

// -------------- For Queues --------------
Expand Down
40 changes: 40 additions & 0 deletions packages/server/src/insights/insight-objects.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {
ChartOutputType,
GanttChartOutputType,
InsightCategories,
InsightFilterOption,
InsightObject,
InsightType,
MultipleGanttChartOutputType,
numToWeekday,
PossibleOutputTypes,
Role,
StringMap,
TableOutputType,
Expand Down Expand Up @@ -1276,6 +1278,44 @@ export const StaffQuestionTimesByDay: InsightObject = {
},
};

//------------------- Insights Across Semesters (and of different sections with the same semester) -------------------//
// TODO: utilize the new supercourse entity to identify the same course across different semesters and sections and
// generate relevant summary statistics to show trends in system usage and adoption over time

export const TotalQuestionsAcrossSemesters: InsightObject = {
displayName: 'Total Questions Asked Acress Semesters',
description: '',
roles: [],
insightType: InsightType.Chart,
insightCategory: 'Other_Semesters',
compute: function ({
insightFilters,
cacheManager,
}: {
insightFilters: any;
cacheManager: Cache;
}): Promise<PossibleOutputTypes> {
throw new Error('Function not implemented.');
},
};

export const TotalQuestionsAcrossSections: InsightObject = {
displayName: 'Total Questions Asked Across Sections',
description: '',
roles: [],
insightType: InsightType.Chart,
insightCategory: 'Other_Sections',
compute: function ({
insightFilters,
cacheManager,
}: {
insightFilters: any;
cacheManager: Cache;
}): Promise<PossibleOutputTypes> {
throw new Error('Function not implemented.');
},
};

export const INSIGHTS_MAP = {
TotalStudents,
TotalQuestionsAsked,
Expand Down
1 change: 0 additions & 1 deletion packages/server/src/insights/insights.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { INSIGHTS_MAP } from './insight-objects';

/**
* Initial Product: Create a system to aggregate certain data within a certain time period for one course
* or accross all courses
*
* Support courseIds and time intervals (no longer than a semester)
*/
Expand Down
69 changes: 51 additions & 18 deletions packages/server/src/organization/organization.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import { RedisProfileService } from '../redisProfile/redis-profile.service';
import { OrgOrCourseRolesGuard } from 'guards/org-or-course-roles.guard';
import { OrgRoles } from 'decorators/org-roles.decorator';
import { CourseRoles } from 'decorators/course-roles.decorator';
import { SuperCourseModel } from 'course/super-course.entity';
import { CourseService } from 'course/course.service';

// TODO: put the error messages in ERROR_MESSAGES object
Expand Down Expand Up @@ -377,17 +378,32 @@ export class OrganizationController {
`Semester ID is invalid`,
HttpStatus.BAD_REQUEST,
);
} else if (courseDetails.semesterId && courseDetails.semesterId !== -1) {
const semester = await manager.findOne(SemesterModel, {
where: { id: courseDetails.semesterId },
relations: ['courses'],
} else if (courseDetails.semesterId && courseDetails.semesterId == -1) {
throw new HttpException(`Semester must be set`, HttpStatus.BAD_REQUEST);
}

const semester = await manager.findOne(SemesterModel, {
where: { id: courseDetails.semesterId },
relations: ['courses'],
});
if (!semester) {
throw new HttpException(`Semester not found`, HttpStatus.NOT_FOUND);
}
newCourse.semester = semester;

const superCourse = await manager.findOne(SuperCourseModel, {
where: { name: newCourse.name },
});
if (!superCourse) {
const newSuperCourse = manager.create(SuperCourseModel, {
name: newCourse.name,
organizationId: oid,
});
if (!semester) {
throw new HttpException(`Semester not found`, HttpStatus.NOT_FOUND);
}
newCourse.semester = semester;
await manager.save(newCourse);
await manager.save(newSuperCourse);
}
newCourse.superCourse = superCourse;

await manager.save(newCourse);

// Create default settings
const newCourseSettings = manager.create(CourseSettingsModel, {
Expand Down Expand Up @@ -505,18 +521,18 @@ export class OrganizationController {
);
} else if (courseDetails.semesterId && courseDetails.semesterId == -1) {
throw new HttpException(`Semester must be set`, HttpStatus.BAD_REQUEST);
} else if (courseDetails.semesterId && courseDetails.semesterId !== -1) {
const semester = await manager.findOne(SemesterModel, {
where: { id: courseDetails.semesterId },
relations: ['courses'],
});
if (!semester) {
throw new HttpException(`Semester not found`, HttpStatus.NOT_FOUND);
}
}

courseInfo.course.semester = semester;
const semester = await manager.findOne(SemesterModel, {
where: { id: courseDetails.semesterId },
relations: ['courses'],
});
if (!semester) {
throw new HttpException(`Semester not found`, HttpStatus.NOT_FOUND);
}

courseInfo.course.semester = semester;

courseInfo.course.name = courseDetails.name;

if (courseDetails.coordinator_email) {
Expand All @@ -530,6 +546,23 @@ export class OrganizationController {
courseInfo.course.zoomLink = courseDetails.zoomLink;
courseInfo.course.timezone = courseDetails.timezone;

// To update production courses with the new super course feature when they update their course
// TODO: remove once all existing production courses have non-null super courses
const standardizedCourseName = courseInfo.course.name
.trim()
.toLowerCase();
const superCourse = await manager.findOne(SuperCourseModel, {
where: { name: standardizedCourseName },
});
if (!superCourse) {
const newSuperCourse = manager.create(SuperCourseModel, {
name: standardizedCourseName,
organizationId: oid,
});
await manager.save(newSuperCourse);
}
courseInfo.course.superCourse = superCourse;

await manager.save(courseInfo.course);
// Remove current professors
await manager.delete(UserCourseModel, {
Expand Down
6 changes: 3 additions & 3 deletions packages/server/src/organization/organization.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,23 +53,23 @@ export class OrganizationModel extends BaseEntity {
@Exclude()
@JoinColumn({ name: 'organizationId' })
@OneToMany(
(type) => OrganizationUserModel,
() => OrganizationUserModel,
(organizationUser) => organizationUser.organization,
)
organizationUsers: OrganizationUserModel[];

@Exclude()
@JoinColumn({ name: 'organizationId' })
@OneToMany(
(type) => OrganizationCourseModel,
() => OrganizationCourseModel,
(organizationCourse) => organizationCourse.organization,
)
organizationCourses: OrganizationCourseModel[];

@Exclude()
@JoinColumn({ name: 'organizationId' })
@OneToMany(
(type) => LMSOrganizationIntegrationModel,
() => LMSOrganizationIntegrationModel,
(integration) => integration.organization,
)
organizationIntegrations: LMSOrganizationIntegrationModel[];
Expand Down
Loading