Skip to content
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

Fix/student work metrics #199

Merged
merged 20 commits into from
Mar 25, 2025
Merged
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: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,6 @@ terraform/.terraform/
*/terraform.tfstate*
*/terraform.tfvars

#Alex env files
backend/.env.alex

# Misc
TODO.txt
3 changes: 3 additions & 0 deletions backend/internal/github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
14 changes: 13 additions & 1 deletion backend/internal/github/sharedclient/sharedclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
32 changes: 31 additions & 1 deletion backend/internal/handlers/classrooms/assignments/assignments.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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,
Expand Down
104 changes: 95 additions & 9 deletions backend/internal/handlers/classrooms/assignments/works/works.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the thought behind this sanity check, but it doesn't seem like it does anything to fix the bad state once you recognize that there may be something wrong with the commit data.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it updates it now!

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,
})
}
}
Expand All @@ -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
Expand All @@ -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,
})
}
}
16 changes: 6 additions & 10 deletions backend/internal/storage/postgres/works.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/api/assignments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
8 changes: 5 additions & 3 deletions frontend/src/api/student_works.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export const getFirstCommit = async (
): Promise<Date> => {
const response = await fetch(
`${base_url}/classrooms/classroom/${classroomID}/assignments/assignment/${assignmentID}/works/work/${studentWorkID}/first-commit`,
{
{
method: "GET",
credentials: "include",
headers: {
Expand All @@ -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 (
Expand Down
20 changes: 20 additions & 0 deletions frontend/src/hooks/useAssignment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getAssignmentIndirectNav,
getAssignments,
getAssignmentTemplate,
getAssignmentTotalCommits,
postAssignmentToken,
} from "@/api/assignments";
import {
Expand Down Expand Up @@ -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.
*
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
19 changes: 7 additions & 12 deletions frontend/src/pages/Assignments/Assignment/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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) {
Expand Down Expand Up @@ -104,11 +102,8 @@ const Assignment: React.FC = () => {
<div className="Assignment__metrics">
<h2>Metrics</h2>
<MetricPanel>
<Metric title="First Commit Date">
{firstCommitDate ? formatDate(firstCommitDate) : "N/A"}
</Metric>
<Metric title="Total Commits">
{totalCommits?.toString() ?? "N/A"}
{totalAssignmentCommits ? totalAssignmentCommits.toString() : 0}
</Metric>
</MetricPanel>

Expand Down
Loading