Skip to content

added export to csv feature to all results pages #192

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
236c766
added export to csv feature to all results pages
AarifLamat Apr 1, 2025
48cbe0f
attempt at creating question-attempt page
AarifLamat Apr 2, 2025
efae044
fixed the QuestionAttemptViewSet and added frontend navigation to the…
AarifLamat Apr 2, 2025
a58c42e
added student answers to question attempts page
AarifLamat Apr 16, 2025
827637a
moved results and ranking outside of test management
AarifLamat Apr 16, 2025
b41d8b9
removed localstorage logic in competition quiz
AarifLamat Apr 16, 2025
d357343
moved results outside of test and each result specific to a quiz
AarifLamat Apr 17, 2025
4b28815
added quizname to results context
AarifLamat Apr 19, 2025
ac290cc
added quizname back to question attempts export
AarifLamat Apr 21, 2025
6f96a6c
added nonpaginated data export to question attempts
AarifLamat Apr 27, 2025
84b2bed
added non paginated data action to team and individual results
AarifLamat Apr 29, 2025
713cf72
trying to ensure team field is populated when quiz attempts are made.…
AarifLamat Apr 30, 2025
6af3071
Merge branch 'main' of https://github.com/codersforcauses/wajo into i…
AarifLamat Apr 30, 2025
d81cc8a
updated results page to use data grid instead of boxes with quiz names
AarifLamat May 10, 2025
8d3c181
attempting to add number of quiz attempts field in admin quiz endpoint
AarifLamat May 13, 2025
1019621
yayay added teamlist page
AarifLamat May 14, 2025
6ab180f
attempt to remove flake8 errors and test errors
AarifLamat May 14, 2025
f1348b7
made test user staff
AarifLamat May 14, 2025
495699d
added quiz_id to team leaderboard tests
AarifLamat May 14, 2025
22ec357
added teamlist page to results id page
AarifLamat May 14, 2025
1af677f
added quiz_id as a query parameter instead of part of request object
AarifLamat May 14, 2025
f1a9d46
er print checks...
AarifLamat May 14, 2025
c61fc36
added quiz attempts endpoint, serializer and frontend. Also added a q…
AarifLamat May 28, 2025
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
56 changes: 55 additions & 1 deletion client/src/components/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { ArrowLeft } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { createContext, useContext, useEffect, useState } from "react";

import Navbar from "@/components/navbar";
import Sidebar from "@/components/sidebar";
import { Button } from "@/components/ui/button";
import Footer from "@/components/ui/footer";
import { WaitingLoader } from "@/components/ui/loading";
import { useAuth } from "@/context/auth-provider";
import { useFetchData } from "@/hooks/use-fetch-data";
import { AdminQuizName } from "@/types/quiz";
import { Role } from "@/types/user";

interface ProtectedPageProps {
Expand Down Expand Up @@ -121,3 +123,55 @@ function NotAuthorizedPage() {
</div>
);
}

// Create a context to store the quiz ID and name for the results pages
interface QuizResultsContextType {
quizId: number;
quizName: string | undefined;
}
// Create a context to store the quiz ID and name for the results pages
const QuizResultsContext = createContext<QuizResultsContextType | undefined>(
undefined,
);

export const useQuizResultsContext = () => {
const context = useContext(QuizResultsContext);
if (!context) {
throw new Error(
"useQuizResultsContext must be used within a QuizResultsContext.Provider",
);
}
return context;
};

export function ResultsLayout({ children }: { children: React.ReactNode }) {
const router = useRouter();
const { id } = router.query;

const quizId = typeof id === "string" ? parseInt(id, 10) : undefined;

// fetch quiz name
const { data, isLoading, error } = useFetchData<AdminQuizName>({
queryKey: [`quiz.admin-quizzes.get_quiz_name.${quizId}`],
endpoint: `/quiz/admin-quizzes/get_quiz_name/?quiz_id=${quizId}`,
});

const quizName = data?.name;

if (!quizId || isNaN(quizId)) {
return <div>Error: Quiz ID is missing or invalid</div>;
}

if (isLoading) {
return <WaitingLoader />;
}

if (error) {
return <div>Error: Failed to fetch quiz data</div>;
}
return (
<QuizResultsContext.Provider value={{ quizId, quizName }}>
<div>{children}</div>
</QuizResultsContext.Provider>
);
}
10 changes: 10 additions & 0 deletions client/src/components/ui/Quiz/generic-quiz.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ export default function GenericQuiz({
);
// console.log("currentAnswer", currentAnswer);
if (currentAnswer) {
console.log("Saving currentAnswer to the backend: ", currentAnswer);
console.log("quizAttempt: ", quizAttempt);

const quizAttemptResult = quizAttempt?.results?.[0];
if (!quizAttemptResult) {
console.error("No quiz attempt found for saving answers.");
toast.error("Failed to save answers: No quiz attempt found.");
return;
}

saveAnswer({
student: quizAttempt.results[0].student,
question: currentAnswer.question,
Expand Down
150 changes: 150 additions & 0 deletions client/src/components/ui/Results/competition-results-data-grid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import Link from "next/link";
import { useRouter } from "next/router";
import * as React from "react";

import { Button } from "@/components/ui/button";
import DateTimeDisplay from "@/components/ui/date-format";
import { SortIcon } from "@/components/ui/icon";
import { WaitingLoader } from "@/components/ui/loading";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { cn } from "@/lib/utils";
import { DatagridProps } from "@/types/data-grid";
import { AdminQuiz } from "@/types/quiz";

/**
* Renders a paginated data grid for displaying competition information.
*
* The `CompetitionResultsDataGrid` component displays a table of competitions with pagination
* and sorting functionality. It allows sorting by the competition status and handles
* pagination of the data. The data grid updates when changes occur in the data context.
*
* @function CompetitionDataGrid
* @template T - The type of data being displayed in the grid, in this case, `Competition`.
* @param {Object} props - The props object.
* @param {Competition[]} props.datacontext - The array of competition data items to be displayed in the grid.
* @param {function(Competition[]): void} props.onDataChange - Callback triggered when the data changes.
* @param {number} props.changePage - The page number to navigate to when the data changes.
*
* @example
* const [competitions, setCompetitions] = useState<Competition[]>([]);
* const [page, setPage] = useState(1);
*
* const handleDataChange = (updatedData: Competition[]) => {
* setCompetitions(updatedData);
* };
*
* return (
* <CompetitionResultsDataGrid
* datacontext={competitions}
* onDataChange={handleDataChange}
* changePage={page}
* />
* );
*/
export function CompetitionResultsDataGrid({
datacontext,
isLoading,
startIdx,
onOrderingChange = () => {},
}: DatagridProps<AdminQuiz>) {
const router = useRouter();

const commonTableHeadClasses = "w-auto text-white text-nowrap";
return (
<div className="grid">
<div className="overflow-hidden rounded-lg border">
<Table className="w-full border-collapse text-left shadow-md">
<TableHeader className="bg-black text-lg font-semibold">
<TableRow className="hover:bg-muted/0">
<TableHead className={commonTableHeadClasses}>No.</TableHead>
<TableHead
className={commonTableHeadClasses}
onClick={() => onOrderingChange("name")}
>
<SortIcon title="Name" />
</TableHead>
{/* <TableHead className={commonTableHeadClasses}>Intro</TableHead> */}
<TableHead className={commonTableHeadClasses}>
Total Marks
</TableHead>
<TableHead className={commonTableHeadClasses}>
Open Date
</TableHead>
<TableHead className={commonTableHeadClasses}>
Time Limit
</TableHead>
<TableHead className={commonTableHeadClasses}>
Time Window
</TableHead>
<TableHead className={commonTableHeadClasses}>Attempts</TableHead>
<TableHead
className={cn(
commonTableHeadClasses,
"sticky right-0 bg-black",
)}
>
Actions
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{!isLoading && datacontext.length > 0 ? (
datacontext.map((item, index) => (
<TableRow
key={item.id}
className={
"divide-gray-200 border-gray-50 text-sm text-black"
}
>
<TableCell className="w-0">
{startIdx ? startIdx + index : item.id}
</TableCell>
<TableCell className="w-1/4">{item.name}</TableCell>
{/* <TableCell className="w-1/2 max-w-80 truncate">
{item.intro}
</TableCell> */}
<TableCell className="w-0">{item.total_marks}</TableCell>
<TableCell className="w-0">
<DateTimeDisplay date={item.open_time_date} />
</TableCell>
<TableCell className="w-0">{item.time_limit}</TableCell>
<TableCell className="w-0">{item.time_window}</TableCell>
<TableCell className="w-0">
{item.quiz_attempt_count}
</TableCell>
<TableCell className="sticky right-0 bg-white">
<Button asChild className="me-1">
<Link href={`${router.pathname}/${item.id}`}>
View Results
</Link>
</Button>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={8}
className="py-4 text-center text-gray-500"
>
{isLoading ? (
<WaitingLoader className="p-0" />
) : (
"No Results Found"
)}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
);
}
152 changes: 152 additions & 0 deletions client/src/components/ui/Results/teamlist-data-grid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import React from "react";

import { SortIcon } from "@/components/ui/icon";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { cn } from "@/lib/utils";
import { DatagridProps } from "@/types/data-grid";
import { TeamLeaderboard } from "@/types/leaderboard";

/**
* Renders a paginated data grid for displaying leaderboard information for teams.
*
* The `TeamListDataGrid` component displays a table with columns for school name, team ID,
* total team marks, country school, maximum year level, and the names of each team member.
* Users can sort the data by clicking on each sortable column. The grid is updated whenever
* the `datacontext` prop changes or a column is re-sorted.
*
* @function TeamListDataGrid
* @template T - The type of data being displayed in the grid, in this case, `TeamLeaderboard`.
* @param {Object} props - The props object.
* @param {TeamLeaderboard[]} props.datacontext - The array of team leaderboard data items to be displayed.
* @param {function(TeamLeaderboard[]): void} [props.onOrderingChange] - Callback triggered when a user clicks a sortable column header. Receives the field name to sort by.
*
*/

export function TeamListDataGrid({
datacontext,
onOrderingChange = () => {},
}: DatagridProps<TeamLeaderboard>) {
const commonTableHeadClasses = "w-auto text-white text-nowrap";

return (
<div className="grid">
<div className="overflow-hidden rounded-lg border">
<Table className="w-full border-collapse text-left shadow-md">
<TableHeader className="bg-black text-lg font-semibold">
<TableRow className="hover:bg-muted/0">
<TableHead
className={cn(commonTableHeadClasses, "rounded-tl-lg")}
>
<div className="flex items-center text-white">
<span>School Name</span>
<span
className="ml-2 cursor-pointer"
onClick={() => onOrderingChange("school__name")}
>
<SortIcon />
</span>
</div>
</TableHead>
<TableHead className={cn(commonTableHeadClasses)}>
<div className="flex items-center text-white">
<span>Team I.D.</span>
<span
className="ml-2 cursor-pointer"
onClick={() => onOrderingChange("id")}
>
<SortIcon />
</span>
</div>
</TableHead>
<TableHead className={cn(commonTableHeadClasses)}>Name</TableHead>
<TableHead className={cn(commonTableHeadClasses)}>Year</TableHead>
<TableHead className={cn(commonTableHeadClasses)}>
Score
</TableHead>
<TableHead className={cn(commonTableHeadClasses)}>Name</TableHead>
<TableHead className={cn(commonTableHeadClasses)}>Year</TableHead>
<TableHead className={cn(commonTableHeadClasses)}>
Score
</TableHead>
<TableHead className={cn(commonTableHeadClasses)}>Name</TableHead>
<TableHead className={cn(commonTableHeadClasses)}>Year</TableHead>
<TableHead className={cn(commonTableHeadClasses)}>
Score
</TableHead>
<TableHead className={cn(commonTableHeadClasses)}>Name</TableHead>
<TableHead className={cn(commonTableHeadClasses)}>Year</TableHead>
<TableHead className={cn(commonTableHeadClasses)}>
Score
</TableHead>
<TableHead className={cn(commonTableHeadClasses)}>
<div className="flex items-center text-white">
<span>Total Marks</span>
<span
className="ml-2 cursor-pointer"
onClick={() => onOrderingChange("total_marks")}
>
<SortIcon />
</span>
</div>
</TableHead>
</TableRow>
</TableHeader>

<TableBody>
{datacontext.length > 0 ? (
datacontext.map((item, index) => {
const studentCells = Array(4)
.fill(null)
.map((_, i) => (
<>
<TableCell key={i} className="whitespace-nowrap">
{item.students?.[i] ? `${item.students[i].name}` : ""}
</TableCell>
<TableCell key={i} className="whitespace-nowrap">
{item.students?.[i]
? `${item.students[i].year_level}`
: ""}
</TableCell>
<TableCell key={i} className="whitespace-nowrap">
{item.students?.[i]
? `${item.students[i].student_score}`
: ""}
</TableCell>
</>
));

return (
<TableRow
key={index}
className="divide-gray-200 border-gray-50 text-sm text-black"
>
<TableCell>{item.school}</TableCell>
<TableCell>{item.id}</TableCell>
{studentCells}
<TableCell>{item.total_marks}</TableCell>
</TableRow>
);
})
) : (
<TableRow>
<TableCell
colSpan={9}
className="py-4 text-center text-gray-500"
>
No Results Found
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
);
}
Loading
Loading