diff --git a/.gitignore b/.gitignore index 20bbcfd7..94274699 100644 --- a/.gitignore +++ b/.gitignore @@ -60,8 +60,6 @@ terraform/.terraform/ */terraform.tfstate* */terraform.tfvars -#Alex env files -backend/.env.alex # Misc TODO.txt diff --git a/backend/internal/github/github.go b/backend/internal/github/github.go index 85a0ee15..44a2d092 100644 --- a/backend/internal/github/github.go +++ b/backend/internal/github/github.go @@ -70,6 +70,9 @@ type GitHubBaseClient interface { //All methods in the SHARED client // Create a new branch in a repository CreateBranch(ctx context.Context, owner, repo, baseBranch, newBranchName string) (*github.Reference, error) + // List the branches in a repository + ListBranches(ctx context.Context, owner string, repo string, opts *github.ListOptions) ([]*github.Branch, error) + // Get the details of a pull request GetPullRequest(ctx context.Context, owner string, repo string, pullNumber int) (*github.PullRequest, error) diff --git a/backend/internal/github/sharedclient/sharedclient.go b/backend/internal/github/sharedclient/sharedclient.go index 849a3495..c41697aa 100644 --- a/backend/internal/github/sharedclient/sharedclient.go +++ b/backend/internal/github/sharedclient/sharedclient.go @@ -43,8 +43,20 @@ func (api *CommonAPI) ListRepositoriesByOrg(ctx context.Context, orgName string, func (api *CommonAPI) ListCommits(ctx context.Context, owner string, repo string, opts *github.CommitsListOptions) ([]*github.RepositoryCommit, error) { commits, _, err := api.Client.Repositories.ListCommits(ctx, owner, repo, opts) + if err != nil { + return nil, fmt.Errorf("error listing commits: %v", err) + } + + return commits, nil +} + +func (api *CommonAPI) ListBranches(ctx context.Context, owner string, repo string, opts *github.ListOptions) ([]*github.Branch, error) { + branches, _, err := api.Client.Repositories.ListBranches(ctx, owner, repo, opts) + if err != nil { + return nil, fmt.Errorf("error listing branches: %v", err) + } - return commits, err + return branches, nil } func (api *CommonAPI) getBranchHead(ctx context.Context, owner, repo, branchName string) (*github.Reference, error) { diff --git a/backend/internal/handlers/classrooms/assignments/assignments.go b/backend/internal/handlers/classrooms/assignments/assignments.go index 35123fa5..dd5b17df 100644 --- a/backend/internal/handlers/classrooms/assignments/assignments.go +++ b/backend/internal/handlers/classrooms/assignments/assignments.go @@ -12,6 +12,7 @@ import ( "github.com/CamPlume1/khoury-classroom/internal/models" "github.com/CamPlume1/khoury-classroom/internal/utils" "github.com/gofiber/fiber/v2" + "github.com/google/go-github/github" "github.com/jackc/pgx/v5" ) @@ -517,16 +518,45 @@ func (s *AssignmentService) GetFirstCommitDate() fiber.Handler { func (s *AssignmentService) GetCommitCount() fiber.Handler { return func(c *fiber.Ctx) error { + classroomID, err := strconv.Atoi(c.Params("classroom_id")) + if err != nil { + return errs.BadRequest(err) + } + assignmentID, err := strconv.Atoi(c.Params("assignment_id")) if err != nil { return errs.BadRequest(err) } - totalCommits, err := s.store.GetTotalWorkCommits(c.Context(), assignmentID) + works, err := s.store.GetWorks(c.Context(), classroomID, assignmentID) if err != nil { return errs.InternalServerError() } + totalCommits := 0 + for _, work := range works { + var branchOpts github.ListOptions + branches, err := s.appClient.ListBranches(c.Context(), work.OrgName, work.RepoName, &branchOpts) + if err != nil { + return errs.GithubAPIError(err) + } + var allCommits []*github.RepositoryCommit + + for _, branch := range branches { + var opts github.CommitsListOptions + // Assumes a single contirbutor, KHO-144 + opts.Author = work.Contributors[0].GithubUsername + opts.SHA = *branch.Name + commits, err := s.appClient.ListCommits(c.Context(), work.OrgName, work.RepoName, &opts) + if err != nil { + return errs.GithubAPIError(err) + } + allCommits = append(allCommits, commits...) + } + totalCommits += len(allCommits) + + } + return c.Status(http.StatusOK).JSON(fiber.Map{ "assignment_id": assignmentID, "total_commits": totalCommits, diff --git a/backend/internal/handlers/classrooms/assignments/works/works.go b/backend/internal/handlers/classrooms/assignments/works/works.go index 66140a06..d0d430fd 100644 --- a/backend/internal/handlers/classrooms/assignments/works/works.go +++ b/backend/internal/handlers/classrooms/assignments/works/works.go @@ -275,9 +275,43 @@ func (s *WorkService) GetCommitCount() fiber.Handler { return err } + totalCount := work.CommitAmount + // Zero either implies bad data or no commits, double check to be safe + if totalCount == 0 { + var branchOpts github.ListOptions + branches, err := s.appClient.ListBranches(c.Context(), work.OrgName, work.RepoName, &branchOpts) + if err != nil { + return errs.GithubAPIError(err) + } + var allCommits []*github.RepositoryCommit + + for _, branch := range branches { + var opts github.CommitsListOptions + // Assumes a single contirbutor, KHO-144 + opts.Author = work.Contributors[0].GithubUsername + opts.SHA = *branch.Name + commits, err := s.appClient.ListCommits(c.Context(), work.OrgName, work.RepoName, &opts) + if err != nil { + return errs.GithubAPIError(err) + } + allCommits = append(allCommits, commits...) + } + totalCount = len(allCommits) + + // If there were commits, update the student work + if totalCount != 0 { + work.StudentWork.CommitAmount = totalCount + _, err := s.store.UpdateStudentWork(c.Context(), work.StudentWork) + if err != nil { + return errs.InternalServerError() + } + } + } + + return c.Status(http.StatusOK).JSON(fiber.Map{ "work_id": work.ID, - "commit_count": work.CommitAmount, + "commit_count": totalCount, }) } } @@ -289,15 +323,29 @@ func (s *WorkService) GetCommitsPerDay() fiber.Handler { return err } - var opts github.CommitsListOptions - opts.Author = work.Contributors[0].GithubUsername - commits, err := s.appClient.ListCommits(c.Context(), work.OrgName, work.RepoName, &opts) - if err != nil { - return errs.GithubAPIError(err) - } + + var branchOpts github.ListOptions + branches, err := s.appClient.ListBranches(c.Context(), work.OrgName, work.RepoName, &branchOpts) + if err != nil { + return errs.GithubAPIError(err) + } + var allCommits []*github.RepositoryCommit + + for _, branch := range branches { + var opts github.CommitsListOptions + // Assumes a single contirbutor, KHO-144 + opts.Author = work.Contributors[0].GithubUsername + opts.SHA = *branch.Name + commits, err := s.appClient.ListCommits(c.Context(), work.OrgName, work.RepoName, &opts) + if err != nil { + return errs.GithubAPIError(err) + } + allCommits = append(allCommits, commits...) + } + commitDatesMap := make(map[time.Time]int) - for _, commit := range commits { + for _, commit := range allCommits { commitDate := commit.GetCommit().GetCommitter().Date if commitDate != nil { // Standardize times to midday UTC @@ -319,9 +367,47 @@ func (s *WorkService) GetFirstCommitDate() fiber.Handler { return err } + fcd := work.FirstCommitDate + + if fcd == nil { + var branchOpts github.ListOptions + branches, err := s.appClient.ListBranches(c.Context(), work.OrgName, work.RepoName, &branchOpts) + if err != nil { + return errs.GithubAPIError(err) + } + fmt.Println(branches) + var allCommits []*github.RepositoryCommit + + for _, branch := range branches { + var opts github.CommitsListOptions + // Assumes a single contirbutor, KHO-144 + opts.Author = work.Contributors[0].GithubUsername + opts.SHA = *branch.Name + commits, err := s.appClient.ListCommits(c.Context(), work.OrgName, work.RepoName, &opts) + if err != nil { + return errs.GithubAPIError(err) + } + allCommits = append(allCommits, commits...) + } + + + + if len(allCommits) > 0 { + fcd = allCommits[len(allCommits)-1].GetCommit().GetCommitter().Date + + work.StudentWork.FirstCommitDate = fcd + _, err := s.store.UpdateStudentWork(c.Context(), work.StudentWork) + if err != nil { + return errs.InternalServerError() + } + + } + + } + return c.Status(http.StatusOK).JSON(fiber.Map{ "work_id": work.ID, - "first_commit_at": work.FirstCommitDate, + "first_commit_at": fcd, }) } } diff --git a/backend/internal/storage/postgres/works.go b/backend/internal/storage/postgres/works.go index bef920c8..e4fe1ecb 100644 --- a/backend/internal/storage/postgres/works.go +++ b/backend/internal/storage/postgres/works.go @@ -221,19 +221,15 @@ func (db *DB) UpdateStudentWork(ctx context.Context, studentWork models.StudentW SET assignment_outline_id = $1, repo_name = $2, unique_due_date = $3, - manual_feedback_score = $4, - auto_grader_score = $5, - grades_published_timestamp = $6, - work_state = $7, - commit_amount = $8, - first_commit_date = $9, - last_commit_date = $10 - WHERE id = $11 + grades_published_timestamp = $4, + work_state = $5, + commit_amount = $6, + first_commit_date = $7, + last_commit_date = $8 + WHERE id = $9 `, studentWork.AssignmentOutlineID, studentWork.RepoName, studentWork.UniqueDueDate, - studentWork.ManualFeedbackScore, - studentWork.AutoGraderScore, studentWork.GradesPublishedTimestamp, studentWork.WorkState, studentWork.CommitAmount, diff --git a/frontend/src/api/assignments.ts b/frontend/src/api/assignments.ts index 6bb20853..a231dda7 100644 --- a/frontend/src/api/assignments.ts +++ b/frontend/src/api/assignments.ts @@ -277,5 +277,6 @@ export const getAssignmentTotalCommits = async ( throw new Error("Network response was not ok"); } const resp = await response.json(); - return resp; + console.log("totoa", resp.total_commits) + return resp.total_commits; }; diff --git a/frontend/src/api/student_works.ts b/frontend/src/api/student_works.ts index cc257ae3..1a9a0b43 100644 --- a/frontend/src/api/student_works.ts +++ b/frontend/src/api/student_works.ts @@ -100,7 +100,7 @@ export const getFirstCommit = async ( ): Promise => { const response = await fetch( `${base_url}/classrooms/classroom/${classroomID}/assignments/assignment/${assignmentID}/works/work/${studentWorkID}/first-commit`, - { + { method: "GET", credentials: "include", headers: { @@ -111,8 +111,10 @@ export const getFirstCommit = async ( if (!response.ok) { throw new Error("Network response was not ok"); } - const resp = ((await response.json()) as Date); - return resp; + const resp = (await response.json()); + const date = resp.first_commit_at as Date + return date; + } export const getTotalCommits = async ( diff --git a/frontend/src/hooks/useAssignment.ts b/frontend/src/hooks/useAssignment.ts index 575e47cf..7b6a5377 100644 --- a/frontend/src/hooks/useAssignment.ts +++ b/frontend/src/hooks/useAssignment.ts @@ -4,6 +4,7 @@ import { getAssignmentIndirectNav, getAssignments, getAssignmentTemplate, + getAssignmentTotalCommits, postAssignmentToken, } from "@/api/assignments"; import { @@ -114,6 +115,25 @@ export const useAssignmentTemplate = (classroomId: number | undefined, assignmen }); }; +/** + * Provides the number of commits made for this assignment + * + * @param classroomId - The ID of the classroom to fetch the template for. + * @param assignmentId - The ID of the assignment to fetch the template for. + * @returns The the number of commits made for this assignment. + */ +export const useAssignmentTotalCommits = (classroomId: number | undefined, assignmentId: number | undefined) => { + return useQuery({ + queryKey: ['totalAssignmentCommits', classroomId, assignmentId], + queryFn: async () => { + if (!classroomId || !assignmentId) return null; + console.log("called...") + return await getAssignmentTotalCommits(classroomId, assignmentId); + }, + enabled: !!classroomId && !!assignmentId + }); +}; + /** * Provides the acceptance metrics for an assignment. * diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 90c96cc6..ae45a563 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -44,8 +44,8 @@ const queryClient = new QueryClient({ }), defaultOptions: { queries: { - staleTime: 5 * 1000, // 5 seconds - gcTime: 5 * 60 * 1000, // 5 minutes + staleTime: 0, // 5 * 1000, // 5 seconds + gcTime: 0, //5 * 60 * 1000, // 5 minutes refetchOnMount: true, retry: 1, }, diff --git a/frontend/src/pages/Assignments/Assignment/index.tsx b/frontend/src/pages/Assignments/Assignment/index.tsx index e61f8107..37e08bd1 100644 --- a/frontend/src/pages/Assignments/Assignment/index.tsx +++ b/frontend/src/pages/Assignments/Assignment/index.tsx @@ -19,7 +19,7 @@ import Pill from "@/components/Pill"; import "./styles.css"; import { StudentWorkState } from "@/types/enums"; import { removeUnderscores } from "@/utils/text"; -import { useAssignment, useStudentWorks, useAssignmentInviteLink, useAssignmentTemplate, useAssignmentMetrics } from "@/hooks/useAssignment"; +import { useAssignment, useStudentWorks, useAssignmentInviteLink, useAssignmentTemplate, useAssignmentMetrics, useAssignmentTotalCommits } from "@/hooks/useAssignment"; import { ErrorToast } from "@/components/Toast"; ChartJS.register(...registerables); @@ -31,21 +31,19 @@ const Assignment: React.FC = () => { const base_url: string = import.meta.env.VITE_PUBLIC_FRONTEND_DOMAIN as string; const { data: assignment } = useAssignment(selectedClassroom?.id, Number(assignmentID)); - const { data: studentWorks, isLoading: isLoadingWorks } = useStudentWorks( + const { data: studentWorks } = useStudentWorks( selectedClassroom?.id, Number(assignmentID) ); const { data: inviteLink = "", error: linkError } = useAssignmentInviteLink(selectedClassroom?.id, assignment?.id, base_url); + + const { data: totalAssignmentCommits } = useAssignmentTotalCommits(selectedClassroom?.id, assignment?.id); const { data: assignmentTemplate, error: templateError } = useAssignmentTemplate(selectedClassroom?.id, assignment?.id); const { acceptanceMetrics, gradedMetrics, error: metricsError } = useAssignmentMetrics(selectedClassroom?.id, Number(assignmentID)); const assignmentTemplateLink = assignmentTemplate ? `https://github.com/${assignmentTemplate.template_repo_owner}/${assignmentTemplate.template_repo_name}` : ""; - const totalCommits = isLoadingWorks ? 0 : studentWorks?.reduce((total, work) => total + work.commit_amount, 0); - const firstCommitDate = isLoadingWorks ? null : studentWorks?.reduce((earliest, work) => { - if (!work.first_commit_date) return earliest; - if (!earliest) return new Date(work.first_commit_date); - return new Date(work.first_commit_date) < earliest ? new Date(work.first_commit_date) : earliest; - }, null as Date | null); + + useEffect(() => { if (linkError || templateError || metricsError) { @@ -104,11 +102,8 @@ const Assignment: React.FC = () => {

Metrics

- - {firstCommitDate ? formatDate(firstCommitDate) : "N/A"} - - {totalCommits?.toString() ?? "N/A"} + {totalAssignmentCommits ? totalAssignmentCommits.toString() : 0} diff --git a/frontend/src/pages/Assignments/StudentSubmission/index.tsx b/frontend/src/pages/Assignments/StudentSubmission/index.tsx index f71eeca3..c2f0f8fa 100644 --- a/frontend/src/pages/Assignments/StudentSubmission/index.tsx +++ b/frontend/src/pages/Assignments/StudentSubmission/index.tsx @@ -116,111 +116,129 @@ const StudentSubmission: React.FC = () => { } }, [selectedClassroom, submission]); - // useEffect for line chart - useEffect(() => { - if (commitsPerDay) { - const sortedDates = Array.from(commitsPerDay.keys()).sort((a, b) => a.valueOf() - b.valueOf()) - // end dates at today or due date, whichever is sooner - if (submission) { - const today = new Date() - today.setUTCHours(0) - today.setUTCMinutes(0) - today.setUTCSeconds(0) - if (sortedDates.length === 0) { - setNoCommits(true) - } else if (sortedDates[sortedDates.length - 1].toDateString() !== (today.toDateString())) { - sortedDates.push(new Date()) - } - } - const sortedCounts: number[] = (sortedDates.map((date) => commitsPerDay.get(date) ?? 0)) - const sortedDatesStrings = sortedDates.map((date) => `${date.getMonth()+1}/${date.getDate()}`) + // retrieves commit data, fills in any days without commits with 0 commits on the day + // returns a list of dates and a cooresponding list of counts + const prepLineData = () => { + function addDays(date: Date, days: number): Date { + const newDate = new Date(date); + newDate.setDate(newDate.getDate() + days); + return newDate; + } - //add in days with 0 commits - const sortedDatesStringsCopy = [...sortedDatesStrings] - let index = 0 - for (let i = 0; i < sortedDatesStringsCopy.length - 1; i++) { + const dates = Array.from(commitsPerDay.keys()); + if (submission) { + const today = new Date() + today.setUTCHours(0) + today.setUTCMinutes(0) + today.setUTCSeconds(0) + + if (dates.length <= 1) { + setNoCommits(true) + return {} + } else if (dates[dates.length - 1].toDateString() !== (today.toDateString())) { + dates.push(today) + } + } - const month = Number(sortedDatesStringsCopy[i].split("/")[0]) - const day = Number(sortedDatesStringsCopy[i].split("/")[1]) - const followingDay = Number(sortedDatesStringsCopy[i + 1].split("/")[1]) + const minDate = new Date(Math.min(...dates.map(date => date.getTime()))); + const maxDate = new Date(Math.max(...dates.map(date => date.getTime()))); + let currentDate = new Date(minDate); + while (currentDate <= maxDate) { + if (!commitsPerDay.has(currentDate)) { + commitsPerDay.set(currentDate, 0); + } + currentDate = addDays(currentDate, 1); + } - const difference = day - followingDay + const sortedEntries = Array.from(commitsPerDay.entries()).sort( + (a, b) => a[0].getTime() - b[0].getTime() + ); + const sortedCommitsPerDay = new Map(sortedEntries); - const adjacent = (difference === -1) - const adjacentWrapped = ((difference === 30 || difference === 29 || difference === 27) && (followingDay === 1)) + if (sortedEntries.keys.length > 0) { + setLoadingAllCommits(false) + } - if (!adjacent && !adjacentWrapped) { - for (let j = 1; j < Math.abs(difference); j++) { - - const nextDay = (new Date(sortedDates[i].getUTCFullYear(), month-1, day)) - nextDay.setDate(nextDay.getDate()+j) + const sortedDates = Array.from(sortedCommitsPerDay.keys()); + const sortedCounts = Array.from(sortedCommitsPerDay.values()); - sortedDatesStrings.splice(index + j, 0, `${nextDay.getUTCMonth()+1}/${nextDay.getDate()}`) - sortedCounts.splice(index + j, 0, 0) - } - index += (Math.abs(difference)) + const sortedDatesStrings = Array.from(sortedDates.map((a) => `${a.getUTCMonth() + 1}/${a.getUTCDate()}`)) + if (sortedDatesStrings.length > 0) { + setLoadingAllCommits(false) + } - } - } + return { sortedDatesStrings, sortedCounts } + } - if (sortedDates.length > 0) { - setLoadingAllCommits(false) - } + // useEffect for line chart + useEffect(() => { + if (commitsPerDay) { - const lineData = { - labels: sortedDatesStrings, - datasets: [ - { - data: sortedCounts, - borderColor: 'rgba(13, 148, 136, 1)', - backgroundColor: 'rgba(75, 192, 192, 0.2)', - tension: 0.05, - }, - ], - } - setLineData(lineData) - - const lineOptions = { - responsive: true, - plugins: { - legend: { - display: false, - }, - title: { - display: false, - }, - datalabels: { - display: false, - }, - }, - scales: { - x: { - grid: { - display: false, + const lineInformation = prepLineData() + const dates = lineInformation.sortedDatesStrings! + const counts = lineInformation.sortedCounts! + + if (dates && counts) { + + if (dates.length > 0 && counts.length > 0) { + const lineData = { + labels: dates, + datasets: [ + { + data: counts, + borderColor: 'rgba(13, 148, 136, 1)', + backgroundColor: 'rgba(75, 192, 192, 0.2)', + tension: 0.05, + }, + ], + } + setLineData(lineData) + + const lineOptions = { + responsive: true, + plugins: { + legend: { + display: false, + }, + title: { + display: false, + }, + datalabels: { + display: false, + }, }, - }, - y: { - grid: { - display: false, + scales: { + x: { + grid: { + display: false, + }, + }, + y: { + grid: { + display: false, + }, + ticks: { + maxTicksLimit: 5, + beginAtZero: true, + }, + }, }, - ticks: { - maxTicksLimit: 5, - beginAtZero: true, + elements: { + point: { + radius: 1, + }, + labels: { + display: false + } }, - }, - }, - elements: { - point: { - radius: 1, - }, - labels: { - display: false } - }, + + setLineOptions(lineOptions) + + } } - setLineOptions(lineOptions) } }, [commitsPerDay]) @@ -276,7 +294,7 @@ const StudentSubmission: React.FC = () => { )} -
{}
+
{ }
);