Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions codewit/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,5 @@ compose.override.yaml
compose.override.yml
docker-compose.override.yaml
docker-compose.override.yml

*.ignore.*
30 changes: 23 additions & 7 deletions codewit/api/src/controllers/course.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,19 @@ async function createCourse(
// eager load the instructors
include: [
Language,
Module,
{
association: Course.associations.modules,
include: [Language, Resource],
through: { attributes: ['ordering'] },
},
{ association: Course.associations.instructors },
{ association: Course.associations.roster },
],
order: [[Module, CourseModules, 'ordering', 'ASC']],
transaction,
});

return formatCourseResponse(course);
return formatCourseResponse(course, true);
});

commit_id(id);
Expand Down Expand Up @@ -186,15 +190,19 @@ async function updateCourse(
await course.reload({
include: [
Language,
Module,
{
association: Course.associations.modules,
include: [Language, Resource],
through: { attributes: ['ordering'] },
},
{ association: Course.associations.instructors },
{ association: Course.associations.roster },
],
order: [[Module, CourseModules, 'ordering', 'ASC']],
transaction,
});

return formatCourseResponse(course);
return formatCourseResponse(course, true);
});
}

Expand All @@ -210,18 +218,26 @@ async function deleteCourse(uid: string): Promise<Course | null> {
return course;
}

async function getCourse(uid: string): Promise<CourseResponse | null> {
async function getCourse(uid: string, is_student: boolean = true): Promise<CourseResponse | null> {
const course = await Course.findByPk(uid, {
include: [
Language,
Module,
{
association: Course.associations.modules,
include: [Language, Resource],
through: { attributes: ['ordering'] },
},
{ association: Course.associations.instructors },
{ association: Course.associations.roster },
],
order: [[Module, CourseModules, 'ordering', 'ASC']],
});

return formatCourseResponse(course);
if (course != null) {
return formatCourseResponse(course, true);
} else {
return null;
}
}

async function getAllCourses(): Promise<CourseResponse[]> {
Expand Down
3 changes: 3 additions & 0 deletions codewit/api/src/models/course.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ class Course extends Model<
declare roster?: NonAttribute<User[]>;
declare registered?: NonAttribute<User[]>;

declare createdAt?: Date;
declare updatedAt?: Date;

declare static associations: {
language: Association<Course, Language>;
modules: Association<Course, Module>;
Expand Down
23 changes: 18 additions & 5 deletions codewit/api/src/routes/course.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import {
updateCourse,
} from '../controllers/course';
import { fromZodError } from 'zod-validation-error';
import {
createCourseSchema,
updateCourseSchema,
import {
createCourseSchema,
updateCourseSchema,
updateEnrollmentFlagsSchema,
bulkRegistrationSchema
bulkRegistrationSchema
} from '@codewit/validations';
import { checkAdmin, checkAuth,checkInstructorOrAdmin } from '../middleware/auth';
import {
Expand Down Expand Up @@ -215,7 +215,7 @@ courseRouter.get('/:courseId/progress', checkAuth, async (req, res) => {
studentName : student.username,
completion : avg,
modulesCompleted : completedModules,
modulesTotal : totalModules,
modulesTotal : totalModules,
};
})
);
Expand Down Expand Up @@ -293,6 +293,19 @@ interface CourseUserStatus {
}

courseRouter.get('/:uid', asyncHandle(async (req, res) => {
if ("admin" in req.query && req.user?.isAdmin) {
// only run if the query is requesting admin level information and the user
// is an admin. don't really have a good way of doing this other than
// creating a new route to handle this
let course = await getCourse(req.params.uid, false);

if (course != null) {
return res.status(200).json(course);
} else {
return res.status(404).json({ error: "CourseNotFound" });
}
}

let check: CourseUserStatus[] = await sequelize.query(
`
select "CourseInstructors"."userUid" is not null as is_instructor,
Expand Down
2 changes: 2 additions & 0 deletions codewit/api/src/typings/response.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export interface CourseResponse {
modules: ModuleResponse[] | number[];
roster: UserResponse[];
instructors: UserResponse[];
createdAt: string,
updatedAt: string,
}

export interface UserResponse {
Expand Down
2 changes: 2 additions & 0 deletions codewit/api/src/utils/responseFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ function formatSingleCourse(course: Course, isGetStudent = false): CourseRespons
: course.modules.map((module) => filterModule(module) as number),
instructors: course.instructors.map((instructor) => filterUser(instructor)),
roster: course.roster.map((user) => filterUser(user)),
createdAt: course.createdAt.toJSON(),
updatedAt: course.updatedAt.toJSON(),
};
}

Expand Down
3 changes: 2 additions & 1 deletion codewit/client/src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Read from '../pages/Read';
import Create from '../pages/Create';
import NotFound from '../components/notfound/NotFound';
import { ExerciseView } from '../pages/create/exercise';
import { AdminCourseRoutes } from "../pages/create/course";
import ModuleForm from '../pages/ModuleForm';
import ResourceForm from '../pages/ResourceForm';
import CourseForm from '../pages/CourseForm';
Expand Down Expand Up @@ -147,7 +148,7 @@ export function App() {
<Route path="exercise/*" element={<ExerciseView/>} />
<Route path="module" element={<ModuleForm />} />
<Route path="resource" element={<ResourceForm />} />
<Route path="course" element={<CourseForm />} />
<Route path="course/*" element={<AdminCourseRoutes/>} />
</Route>
<Route
path="/:courseId/dashboard"
Expand Down
56 changes: 34 additions & 22 deletions codewit/client/src/components/form/ReusableTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface ReusableTableProps<T> {
className?: string,
itemsPerPage?: number,
onEdit?: (item: T) => void,
onDelete: (item: T) => void,
onDelete?: (item: T) => void,
}

const getNestedValue = (obj: any, path: string): any => {
Expand Down Expand Up @@ -63,6 +63,7 @@ export const ReusableTable = <T extends { id?: string | number; uid?: string | n
const [currentPage, setCurrentPage] = useState(1);

const totalPages = Math.ceil(data.length / itemsPerPage);
const enable_actions = onEdit != null || onDelete != null;

const currentData = useMemo(() => {
const startIndex = (currentPage - 1) * itemsPerPage;
Expand All @@ -82,9 +83,13 @@ export const ReusableTable = <T extends { id?: string | number; uid?: string | n
{col.header}
</Table.HeadCell>
))}
<Table.HeadCell className="text-gray-300 font-semibold">
{enable_actions ?
<Table.HeadCell className="text-gray-300 font-semibold">
Actions
</Table.HeadCell>
</Table.HeadCell>
:
null
}
</Table.Head>
<Table.Body className="divide-y divide-gray-700">
{currentData.map((item, rowIdx) => (
Expand All @@ -111,25 +116,32 @@ export const ReusableTable = <T extends { id?: string | number; uid?: string | n
</Table.Cell>
);
})}

<Table.Cell className="text-right space-x-2">
{onEdit != null ?
<button
onClick={() => onEdit(item)}
className="text-sm font-medium text-blue-500 hover:text-blue-400 hover:underline transition-all rounded-md"
>
Edit
</button>
:
null
}
<button
onClick={() => onDelete(item)}
className="text-sm font-medium text-red-500 hover:text-red-400 hover:underline transition-all rounded-md"
>
Delete
</button>
</Table.Cell>
{enable_actions ?
<Table.Cell className="text-right space-x-2">
{onEdit != null ?
<button
onClick={() => onEdit(item)}
className="text-sm font-medium text-blue-500 hover:text-blue-400 hover:underline transition-all rounded-md"
>
Edit
</button>
:
null
}
{onDelete != null ?
<button
onClick={() => onDelete(item)}
className="text-sm font-medium text-red-500 hover:text-red-400 hover:underline transition-all rounded-md"
>
Delete
</button>
:
null
}
</Table.Cell>
:
null
}
</Table.Row>
))}
</Table.Body>
Expand Down
31 changes: 28 additions & 3 deletions codewit/client/src/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,42 @@ import {
ConfirmDelete,
ConfirmAway
} from "./form/button";
import {
TextField,
} from "./form/input";
import {
CheckboxField,
} from "./form/checkbox";
import {
SelectField,
LanguageSelectField,
} from "./form/select";
import {
SubmitIndicator
} from "./form/indicator";

const { useAppForm } = createFormHook({
const { useAppForm, withForm, ...rest } = createFormHook({
fieldContext,
formContext,
fieldComponents: {},
fieldComponents: {
// general input fields
TextField,
CheckboxField,
SelectField,

// predefined input fields
LanguageSelectField,
},
formComponents: {
// action buttons
SubmitButton,
ConfirmReset,
ConfirmDelete,
ConfirmAway,

// indicators
SubmitIndicator,
},
});

export { useAppForm, useFieldContext, useFormContext };
export { useAppForm, useFieldContext, useFormContext, withForm };
23 changes: 19 additions & 4 deletions codewit/client/src/form/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,34 @@ import { useFormContext } from "./context";

interface SubmitButtonProps {}

export function SubmitButton({}) {
/**
* a simple save button that will be disabled when the form is submitting, the
* form has not changed, and if the form will allow the submit (eg if the form
* does not have errors that the user needs to fix)
*/
export function SubmitButton({}: SubmitButtonProps) {
const form = useFormContext();

return <form.Subscribe selector={state => ({
submitting: state.isSubmitting,
dirty: state.isDirty,
can_submit: state.canSubmit,
})}>
{({submitting, dirty, can_submit}) => (
<Button type="submit" disabled={submitting || !dirty || !can_submit}>
{({submitting, dirty, can_submit}) => {
let title = undefined;

if (!can_submit) {
title = "There are errors with the form and must be fixed before submitting";
}

return <Button
type="submit"
title={title}
disabled={submitting || !dirty || !can_submit}
>
Save
</Button>
)}
}}
</form.Subscribe>
}

Expand Down
Loading
Loading