github.com/quickfeed/quickfeed@v0.0.0-20240507093252-ed8ca812a09c/assignments/tasks.go (about) 1 package assignments 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 8 "github.com/quickfeed/quickfeed/database" 9 "github.com/quickfeed/quickfeed/qf" 10 "github.com/quickfeed/quickfeed/scm" 11 ) 12 13 // taskName returns the task name based on the filename 14 // excluding the task- prefix and the .md suffix. 15 func taskName(filename string) string { 16 taskName := filename[len("task-"):] 17 return taskName[:len(taskName)-len(".md")] 18 } 19 20 // newTask returns a task from markdown contents and associates it with the given assignment. 21 // The provided markdown contents must contain a title specified on the first line, 22 // starting with the "# " character sequence, followed by two new line characters. 23 func newTask(contents []byte, assignmentOrder uint32, name string) (*qf.Task, error) { 24 if !bytes.HasPrefix(contents, []byte("# ")) { 25 return nil, fmt.Errorf("task with name: %s, does not start with a # title marker", name) 26 } 27 bodyIndex := bytes.Index(contents, []byte("\n\n")) 28 if bodyIndex == -1 { 29 return nil, fmt.Errorf("failed to find task body in task: %s", name) 30 } 31 32 return &qf.Task{ 33 AssignmentOrder: assignmentOrder, 34 Title: string(contents[2:bodyIndex]), 35 Body: string(contents[bodyIndex+2:]), 36 Name: name, 37 }, nil 38 } 39 40 // tasksFromAssignments returns a map, mapping each assignment-order to a map of tasks. 41 func tasksFromAssignments(assignments []*qf.Assignment) map[uint32]map[string]*qf.Task { 42 taskMap := make(map[uint32]map[string]*qf.Task) 43 for _, assignment := range assignments { 44 temp := make(map[string]*qf.Task) 45 for _, task := range assignment.Tasks { 46 temp[task.Name] = task 47 } 48 taskMap[assignment.Order] = temp 49 } 50 return taskMap 51 } 52 53 // mapTasksByID transforms the given tasks to a map from taskID to task. 54 func mapTasksByID(tasks []*qf.Task) map[uint64]*qf.Task { 55 taskMap := make(map[uint64]*qf.Task) 56 for _, task := range tasks { 57 taskMap[task.ID] = task 58 } 59 return taskMap 60 } 61 62 // synchronizeTasksWithIssues synchronizes tasks with issues on SCM's group repositories. 63 func synchronizeTasksWithIssues(ctx context.Context, db database.Database, sc scm.SCM, course *qf.Course, assignments []*qf.Assignment) error { 64 tasksFromTestsRepo := tasksFromAssignments(assignments) 65 createdTasks, updatedTasks, err := db.SynchronizeAssignmentTasks(course, tasksFromTestsRepo) 66 if err != nil { 67 return err 68 } 69 70 repos, err := db.GetRepositoriesWithIssues(&qf.Repository{ 71 ScmOrganizationID: course.GetScmOrganizationID(), 72 }) 73 if err != nil { 74 return err 75 } 76 77 // Creates, updates and deletes issues on all group repositories, based on how tasks differ from last push. 78 // The created issues will be created by the QuickFeed user (the App owner). 79 var createdIssues []*qf.Issue 80 for _, repo := range repos { 81 if !repo.IsGroupRepo() { 82 continue 83 } 84 repoCreatedIssues, err := createIssues(ctx, sc, course, repo, createdTasks) 85 if err != nil { 86 return err 87 } 88 createdIssues = append(createdIssues, repoCreatedIssues...) 89 if err = updateIssues(ctx, sc, course, repo, updatedTasks); err != nil { 90 return err 91 } 92 } 93 // Create issues in the database based on issues created on the scm. 94 return db.CreateIssues(createdIssues) 95 } 96 97 // createIssues creates issues on scm based on repository, course and tasks. Returns created issues. 98 func createIssues(ctx context.Context, sc scm.SCM, course *qf.Course, repo *qf.Repository, tasks []*qf.Task) ([]*qf.Issue, error) { 99 var createdIssues []*qf.Issue 100 for _, task := range tasks { 101 issueOptions := &scm.IssueOptions{ 102 Organization: course.GetScmOrganizationName(), 103 Repository: repo.Name(), 104 Title: task.Title, 105 Body: task.Body, 106 } 107 scmIssue, err := sc.CreateIssue(ctx, issueOptions) 108 if err != nil { 109 return nil, err 110 } 111 createdIssues = append(createdIssues, &qf.Issue{ 112 RepositoryID: repo.ID, 113 TaskID: task.ID, 114 ScmIssueNumber: uint64(scmIssue.Number), 115 }) 116 } 117 return createdIssues, nil 118 } 119 120 // updateIssues updates issues based on repository, course and tasks. It handles deleted tasks by closing them and inserting a statement into the body. 121 func updateIssues(ctx context.Context, sc scm.SCM, course *qf.Course, repo *qf.Repository, tasks []*qf.Task) error { 122 taskMap := mapTasksByID(tasks) 123 for _, issue := range repo.Issues { 124 task, ok := taskMap[issue.TaskID] 125 if !ok { 126 // Issue does not need to be updated 127 continue 128 } 129 issueOptions := &scm.IssueOptions{ 130 Organization: course.GetScmOrganizationName(), 131 Repository: repo.Name(), 132 Title: task.Title, 133 Body: task.Body, 134 Number: int(issue.ScmIssueNumber), 135 } 136 if task.IsDeleted() { 137 issueOptions.State = "closed" 138 } 139 140 if _, err := sc.UpdateIssue(ctx, issueOptions); err != nil { 141 return err 142 } 143 } 144 return nil 145 }