Skip to content

Commit

Permalink
feat: User context for schedules
Browse files Browse the repository at this point in the history
  • Loading branch information
mathhulk committed Jul 3, 2024
1 parent 0575add commit ef62209
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 99 deletions.
Empty file.
8 changes: 1 addition & 7 deletions backend/src/generated-types/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,11 +403,6 @@ export type QueryScheduleByIdArgs = {
};


export type QuerySchedulesByUserArgs = {
created_by: Scalars['String']['input'];
};


export type QuerySectionArgs = {
classNumber: Scalars['String']['input'];
courseNumber: Scalars['String']['input'];
Expand Down Expand Up @@ -445,7 +440,6 @@ export type Schedule = {

export type ScheduleInput = {
courses?: InputMaybe<Array<SelectedCourseInput>>;
created_by: Scalars['String']['input'];
custom_events?: InputMaybe<Array<CustomEventInput>>;
is_public?: InputMaybe<Scalars['Boolean']['input']>;
name?: InputMaybe<Scalars['String']['input']>;
Expand Down Expand Up @@ -867,7 +861,7 @@ export type QueryResolvers<ContextType = any, ParentType extends ResolversParent
grade?: Resolver<Maybe<ResolversTypes['Grade']>, ParentType, ContextType, RequireFields<QueryGradeArgs, 'courseNum' | 'subject'>>;
ping?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
scheduleByID?: Resolver<Maybe<ResolversTypes['Schedule']>, ParentType, ContextType, RequireFields<QueryScheduleByIdArgs, 'id'>>;
schedulesByUser?: Resolver<Maybe<Array<Maybe<ResolversTypes['Schedule']>>>, ParentType, ContextType, RequireFields<QuerySchedulesByUserArgs, 'created_by'>>;
schedulesByUser?: Resolver<Maybe<Array<Maybe<ResolversTypes['Schedule']>>>, ParentType, ContextType>;
section?: Resolver<Maybe<ResolversTypes['Section']>, ParentType, ContextType, RequireFields<QuerySectionArgs, 'classNumber' | 'courseNumber' | 'sectionNumber' | 'subject' | 'term'>>;
terms?: Resolver<Maybe<Array<Maybe<ResolversTypes['Term']>>>, ParentType, ContextType>;
user?: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType>;
Expand Down
93 changes: 61 additions & 32 deletions backend/src/modules/schedule/controller.ts
Original file line number Diff line number Diff line change
@@ -1,74 +1,103 @@
import { formatSchedule } from "./formatter";
import { Schedule, ScheduleInput, CustomEventInput, SelectedCourseInput } from "../../generated-types/graphql";
import {
Schedule,
ScheduleInput,
CustomEventInput,
SelectedCourseInput,
} from "../../generated-types/graphql";
import { ScheduleModel } from "../../models/schedule";
import { omitBy } from "lodash";


// get the schedules for a user
export async function getSchedulesByUser(userID: string): Promise<Schedule[]> {
const userSchedules = await ScheduleModel.find({created_by: userID})
export async function getSchedulesByUser(
context: any
): Promise<Schedule[] | null> {
if (!context.user._id) throw new Error("User is not authenticated");

const userSchedules = await ScheduleModel.find({
created_by: context.user._id,
});
if (userSchedules.length == 0) {
throw new Error("No schedules found for this user")
throw new Error("No schedules found for this user");
}
return userSchedules.map(formatSchedule)
return userSchedules.map(formatSchedule);
}

// get the schedule for a user and a specific term
export async function getScheduleByID(id: string): Promise<Schedule> {
const scheduleFromID = await ScheduleModel.findById({_id: id})
const scheduleFromID = await ScheduleModel.findById({ _id: id });
if (!scheduleFromID) {
throw new Error("No schedules found with this ID")
throw new Error("No schedules found with this ID");
}
return formatSchedule(scheduleFromID)
return formatSchedule(scheduleFromID);
}

// delete a schedule specified by ObjectID
export async function removeSchedule(scheduleID: string): Promise<string> {
const deletedSchedule = await ScheduleModel.findByIdAndDelete(scheduleID)
const deletedSchedule = await ScheduleModel.findByIdAndDelete(scheduleID);
if (!deletedSchedule) {
throw new Error("Schedule deletion failed")
throw new Error("Schedule deletion failed");
}
return scheduleID
return scheduleID;
}

function removeNullEventVals(custom_event: CustomEventInput) {
for (const key in custom_event) {
if (custom_event[key as keyof CustomEventInput] === null) {
delete custom_event[key as keyof CustomEventInput];
}
if (custom_event[key as keyof CustomEventInput] === null) {
delete custom_event[key as keyof CustomEventInput];
}
}
}

// create a new schedule
//export async function createSchedule(created_by: string, term: TermInput, schedule_name: string | undefined | null, is_public: boolean | null | undefined, class_IDs: string[] | undefined | null, primary_section_IDs: string[] | undefined | null, secondary_section_IDs: string[] | undefined | null): Promise<Schedule> {
export async function createSchedule(main_schedule: ScheduleInput): Promise<Schedule> {
export async function createSchedule(
main_schedule: ScheduleInput,
context: any
): Promise<Schedule> {
if (!context.user._id) throw new Error("User is not authenticated");
if (main_schedule.custom_events) {
main_schedule.custom_events.forEach(removeNullEventVals)
main_schedule.custom_events.forEach(removeNullEventVals);
}
const non_null_schedule = omitBy(main_schedule, (value) => value == null)
const newSchedule = await ScheduleModel.create(non_null_schedule)
return formatSchedule(newSchedule)
const non_null_schedule = omitBy(main_schedule, (value) => value == null);
const newSchedule = await ScheduleModel.create({
...non_null_schedule,
created_by: context.user._id,
});
return formatSchedule(newSchedule);
}


// update an existing schedule
export async function editSchedule(schedule_ID: string, main_schedule: ScheduleInput): Promise<Schedule> {
export async function editSchedule(
schedule_ID: string,
main_schedule: ScheduleInput
): Promise<Schedule> {
if (main_schedule.custom_events) {
main_schedule.custom_events.forEach(removeNullEventVals)
main_schedule.custom_events.forEach(removeNullEventVals);
}
const non_null_schedule = omitBy(main_schedule, (value) => value == null)
const updatedSchedule = await ScheduleModel.findByIdAndUpdate(schedule_ID, non_null_schedule, {returnDocument: 'after'})
const non_null_schedule = omitBy(main_schedule, (value) => value == null);
const updatedSchedule = await ScheduleModel.findByIdAndUpdate(
schedule_ID,
non_null_schedule,
{ returnDocument: "after" }
);
if (!updatedSchedule) {
throw new Error("Unable to update existing schedule")
throw new Error("Unable to update existing schedule");
}
return formatSchedule(updatedSchedule)
return formatSchedule(updatedSchedule);
}

// update class selection in an existing schedule
export async function setClasses(scheduleID: string, courses: SelectedCourseInput[]): Promise<Schedule> {
const existingSchedule = await ScheduleModel.findByIdAndUpdate(scheduleID, {courses: courses}, {returnDocument: 'after'})
export async function setClasses(
scheduleID: string,
courses: SelectedCourseInput[]
): Promise<Schedule> {
const existingSchedule = await ScheduleModel.findByIdAndUpdate(
scheduleID,
{ courses: courses },
{ returnDocument: "after" }
);
if (!existingSchedule) {
throw new Error("Unable to update existing schedule's class selection")
throw new Error("Unable to update existing schedule's class selection");
}
return formatSchedule(existingSchedule)
return formatSchedule(existingSchedule);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export namespace ScheduleModule {
interface DefinedInputFields {
CustomEventInput: 'start_time' | 'end_time' | 'title' | 'location' | 'description' | 'days_of_week';
SelectedCourseInput: 'class_ID' | 'primary_section_ID' | 'secondary_section_IDs';
ScheduleInput: 'name' | 'created_by' | 'courses' | 'is_public' | 'term' | 'custom_events';
ScheduleInput: 'name' | 'courses' | 'is_public' | 'term' | 'custom_events';
};

export type TermOutput = Pick<Types.TermOutput, DefinedFields['TermOutput']>;
Expand Down
48 changes: 34 additions & 14 deletions backend/src/modules/schedule/resolver.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,50 @@
import { getSchedulesByUser, getScheduleByID, removeSchedule, createSchedule, setClasses, editSchedule } from "./controller";
import {
getSchedulesByUser,
getScheduleByID,
removeSchedule,
createSchedule,
setClasses,
editSchedule,
} from "./controller";
import { ScheduleModule } from "./generated-types/module-types";
import { ScheduleInput, SelectedCourseInput } from "../../generated-types/graphql";
import {
ScheduleInput,
SelectedCourseInput,
} from "../../generated-types/graphql";

const resolvers: ScheduleModule.Resolvers = {
Query: {
schedulesByUser(_parent, args: { created_by: string }) {
return getSchedulesByUser(args.created_by);
schedulesByUser(_parent, _args, context) {
return getSchedulesByUser(context);
},
scheduleByID(_parent, args: { id: string }) {
return getScheduleByID(args.id);
}
},
},
Mutation: {
removeScheduleByID(_parent, args: {id: string}) {
removeScheduleByID(_parent, args: { id: string }) {
return removeSchedule(args.id);
},
createNewSchedule(_parent, args: {main_schedule: ScheduleInput}) {
return createSchedule(args.main_schedule)
createNewSchedule(
_parent,
args: { main_schedule: ScheduleInput },
context
) {
return createSchedule(args.main_schedule, context);
},
editExistingSchedule(
_parent,
args: { id: string; main_schedule: ScheduleInput }
) {
return editSchedule(args.id, args.main_schedule);
},
editExistingSchedule(_parent, args: {id: string, main_schedule: ScheduleInput}) {
return editSchedule(args.id, args.main_schedule)
setSelectedClasses(
_parent,
args: { id: string; courses: SelectedCourseInput[] }
) {
return setClasses(args.id, args.courses);
},
setSelectedClasses(_parent, args: {id: string, courses: SelectedCourseInput[]}) {
return setClasses(args.id, args.courses)
}
}
},
};

export default resolvers;
15 changes: 7 additions & 8 deletions backend/src/modules/schedule/typedefs/schedule.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { gql } from "graphql-tag";

const typedef = gql`
type TermOutput {
year: Int!
semester: String!
Expand All @@ -24,10 +23,9 @@ const typedef = gql`
input ScheduleInput {
name: String
created_by: String!,
courses: [SelectedCourseInput!],
is_public: Boolean,
term: TermInput!,
courses: [SelectedCourseInput!]
is_public: Boolean
term: TermInput!
custom_events: [CustomEventInput!]
}
Expand Down Expand Up @@ -92,7 +90,7 @@ const typedef = gql`
"""
Takes in a user's email and returns all the schedules they created.
"""
schedulesByUser(created_by: String!): [Schedule] @auth
schedulesByUser: [Schedule] @auth
"""
Takes in a schedule's ObjectID and returns a specific schedule.
"""
Expand All @@ -115,8 +113,9 @@ const typedef = gql`
"""
For the schedule specified by the ID, modifies the courses field and returns the updated schedule.
"""
setSelectedClasses(id: ID!, courses: [SelectedCourseInput!]!): Schedule @auth
setSelectedClasses(id: ID!, courses: [SelectedCourseInput!]!): Schedule
@auth
}
`;

export default typedef;
export default typedef;
3 changes: 1 addition & 2 deletions frontend/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ type Query {
"""
Takes in a user's email and returns all the schedules they created.
"""
schedulesByUser(created_by: String!): [Schedule]
schedulesByUser: [Schedule!]

"""
Takes in a schedule's ObjectID and returns a specific schedule.
Expand Down Expand Up @@ -675,7 +675,6 @@ input SelectedCourseInput {

input ScheduleInput {
name: String
created_by: String!
courses: [SelectedCourseInput!]
is_public: Boolean
term: TermInput!
Expand Down
59 changes: 32 additions & 27 deletions frontend/src/app/Schedules/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,17 @@ import { Link } from "react-router-dom";

import Button from "@/components/Button";
import {
AccountResponse,
CREATE_SCHEDULE,
CreateScheduleResponse,
DELETE_SCHEDULE,
GET_ACCOUNT,
GET_SCHEDULES,
GetSchedulesResponse,
} from "@/lib/api";

import styles from "./Schedules.module.scss";

export default function Schedules() {
const { data: account } = useQuery<AccountResponse>(GET_ACCOUNT);

const { data: schedules } = useQuery<GetSchedulesResponse>(GET_SCHEDULES, {
variables: {
createdBy: account?.user.email,
},
});
const { data: schedules } = useQuery<GetSchedulesResponse>(GET_SCHEDULES);

const [createSchedule] = useMutation<CreateScheduleResponse>(
CREATE_SCHEDULE,
Expand All @@ -33,33 +25,46 @@ export default function Schedules() {
year: 2024,
semester: "Spring",
},
createdBy: account?.user.email,
},
update(cache, { data }) {
const schedule = data?.createNewSchedule;

if (!schedule) return;

cache.modify({
fields: {
schedulesByUser(existingSchedules = []) {
const ref = cache.writeFragment({
data: schedule,
fragment: gql`
fragment NewSchedule on Schedule {
_id
name
term {
year
semester
const queryResult = cache.readQuery({
query: GET_SCHEDULES,
});

// Initialize the cache
if (queryResult) {
cache.modify({
fields: {
schedulesByUser: (existingSchedules = []) => {
const ref = cache.writeFragment({
data: schedule,
fragment: gql`
fragment NewSchedule on Schedule {
_id
name
term {
year
semester
}
}
}
`,
});
`,
});

return [...existingSchedules, ref];
return [...existingSchedules, ref];
},
},
},
});

return;
}

cache.writeQuery({
query: GET_SCHEDULES,
data: { schedulesByUser: [schedule] },
});
},
}
Expand Down
Loading

0 comments on commit ef62209

Please sign in to comment.