Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main'
Browse files Browse the repository at this point in the history
  • Loading branch information
OliverLok committed May 5, 2024
2 parents ea4ea76 + dcc7c26 commit fb34694
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 4 deletions.
14 changes: 14 additions & 0 deletions cmd/send-email/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import (
"github.com/alecthomas/kong"

"github.com/edulinq/autograder/config"
"github.com/edulinq/autograder/db"
"github.com/edulinq/autograder/email"
"github.com/edulinq/autograder/log"
)

var args struct {
config.ConfigArgs
Course string `help:"Optional Course ID. Only required when roles or * (all course users) are in the recipients." arg:"" optional:""`
To []string `help:"Email recipents." required:""`
Subject string `help:"Email subject." required:""`
Body string `help:"Email body." required:""`
Expand All @@ -25,6 +27,18 @@ func main() {
log.Fatal("Could not load config options.", err);
}

db.MustOpen();
defer db.MustClose();

if (args.Course != "") {
course := db.MustGetCourse(args.Course);

args.To, err = db.ResolveUsers(course, args.To);
if (err != nil) {
log.Fatal("Failed to resolve users.", err, course);
}
}

err = email.Send(args.To, args.Subject, args.Body, false);
if (err != nil) {
log.Fatal("Could not send email.", err);
Expand Down
64 changes: 63 additions & 1 deletion db/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package db
import (
"errors"
"fmt"
"slices"
"strings"

"github.com/edulinq/autograder/log"
"github.com/edulinq/autograder/model"
Expand Down Expand Up @@ -111,7 +113,6 @@ func SyncUser(course *model.Course, user *model.User,
return SyncUsers(course, newUsers, merge, dryRun, sendEmails);
}


// Sync (merge) new users with existing users.
// The db takes ownership of the passed-in users (they may be modified).
// If |merge| is true, then existing users will be updated with non-empty fields.
Expand Down Expand Up @@ -221,3 +222,64 @@ func SyncUsers(course *model.Course, newUsers map[string]*model.User,

return syncResult, nil;
}

// ResolveUsers maps string representations of roles and * (all roles) to the emails for users with those roles.
// The function takes a course and a list of strings, containing emails, roles, and * as input and returns a sorted slice of lowercase emails without duplicates.
func ResolveUsers(course *model.Course, emails []string) ([]string, error) {
if (backend == nil) {
return nil, fmt.Errorf("Database has not been opened.");
}

emailSet := map[string]any{};
roleSet := map[string]any{};

// Iterate over all strings, checking for emails, roles, and * (which denotes all users).
for _, email := range emails {
email = strings.ToLower(strings.TrimSpace(email));
if (email == "") {
continue;
}

if (strings.Contains(email, "@")) {
emailSet[email] = nil;
} else {
if (email == "*") {
allRoles := model.GetAllRoleStrings();
for role := range allRoles {
roleSet[role] = nil;
}
} else {
if (model.GetRole(email) == model.RoleUnknown) {
log.Warn("Invalid role given to ResolveUsers.", course, log.NewAttr("role", email))
continue;
}

roleSet[email] = nil;
}
}
}

if (len(roleSet) > 0) {
users, err := GetUsers(course);
if (err != nil) {
return nil, err;
}

for _, user := range users {
// Add a user if their role is set.
_, ok := roleSet[model.GetRoleString(user.Role)];
if (ok) {
emailSet[strings.ToLower(user.Email)] = nil;
}
}
}

emailSlice := make([]string, 0, len(emailSet));
for email := range emailSet {
emailSlice = append(emailSlice, email);
}

slices.Sort(emailSlice);

return emailSlice, nil;
}
177 changes: 177 additions & 0 deletions db/user_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package db

import (
"reflect"
"testing"
"time"

"github.com/edulinq/autograder/email"
"github.com/edulinq/autograder/log"
"github.com/edulinq/autograder/model"
"github.com/edulinq/autograder/util"
)
Expand All @@ -14,6 +17,180 @@ type SyncNewUsersTestCase struct {
sendEmails bool
}

func (this *DBTests) DBTestResolveUsers(test *testing.T) {
defer ResetForTesting();

oldValue := log.SetBackgroundLogging(false);
defer log.SetBackgroundLogging(oldValue);

log.SetLevels(log.LevelOff, log.LevelWarn);
defer log.SetLevelFatal();

// Wait for old logs to get written.
time.Sleep(10 * time.Millisecond);

Clear();
defer Clear();

testCases := []struct {input []string; expectedOutput []string; addUsers []*model.User; removeUsers []string; numWarnings int} {
// This test case tests the empty slice input.
{
[]string{},
[]string{},
nil,
[]string{},
0,
},

// This is a simple test case for the empty string input.
{
[]string{""},
[]string{},
nil,
[]string{},
0,
},

// This is a test to ensure the output is sorted.
{
[]string{"[email protected]", "[email protected]", "[email protected]"},
[]string{"[email protected]", "[email protected]", "[email protected]"},
nil,
[]string{},
0,
},

// This is a test to ensure miscapitalized emails only get returned once.
{
[]string{"[email protected]", "[email protected]", "[email protected]"},
[]string{"[email protected]"},
nil,
[]string{},
0,
},

// This is a basic test to ensure that a role gets mapped to the correct email.
{
[]string{"admin"},
[]string{"[email protected]"},
nil,
[]string{},
0,
},

// This is a test for our all roles character, the *.
{
[]string{"*"},
[]string{"[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]"},
nil,
[]string{},
0,
},


// This test case is given redundant roles and emails.
// It tests to ensures we do not produce duplicates on this input.
{
[]string{"other", "*", "[email protected]"},
[]string{"[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]"},
nil,
[]string{},
0,
},

// This test case tests if miscapitalized roles still function.
{
[]string{"OTHER"},
[]string{"[email protected]"},
nil,
[]string{},
0,
},

// This test case tests if warnings are issued on invalid roles.
{
[]string{"trash", "garbage", "waste", "recycle!"},
[]string{},
nil,
[]string{},
4,
},

// This test adds new Users to the course and ensures we retrieve all emails for the given role.
{
[]string{"student"},
[]string{"[email protected]", "[email protected]", "[email protected]"},
[]*model.User{model.NewUser("[email protected]", "", model.GetRole("student")), model.NewUser("[email protected]", "", model.GetRole("student"))},
[]string{},
0,
},

// This is a test case to see if we properly trim whitespace.
{
[]string{"\t\n student ", "\n \t [email protected]", "\t\n \t \n"},
[]string{"[email protected]", "[email protected]"},
nil,
[]string{},
0,
},

// This test case removes the only user from the "owner" role, so we check that a role without any users still functions properly.
{
[]string{"owner", "student"},
[]string{"[email protected]"},
nil,
[]string{"[email protected]"},
0,
},

// This test supplies a single role that resolves to nothing.
{
[]string{"owner"},
[]string{},
nil,
[]string{"[email protected]"},
0,
},
};

for i, testCase := range testCases {
ResetForTesting();
course := MustGetCourse(TEST_COURSE_ID);

for _, newUser := range testCase.addUsers {
SaveUser(course, newUser);
}

for _, removeUser := range testCase.removeUsers {
RemoveUser(course, removeUser);
}

actualOutput, err := ResolveUsers(course, testCase.input);
if (err != nil) {
test.Errorf("Case %d (%+v): Resolve User failed: '%v'.", i, testCase, err);
continue;
}

if (!reflect.DeepEqual(testCase.expectedOutput, actualOutput)) {
test.Errorf("Case %d (%+v): Incorrect Output. Expected: '%v', Actual: '%v'.", i,
testCase, testCase.expectedOutput, actualOutput);
continue;
}

logs, err := GetLogRecords(log.LevelWarn, time.Time{}, "", "", "")
if (err != nil) {
test.Errorf("Case %d (%+v): Error getting log records.", i, testCase);
continue;
}

if (testCase.numWarnings != len(logs)) {
test.Errorf("Case %d (%+v): Incorrect number of warnings issued. Expected: %d, Actual: %d.", i,
testCase, testCase.numWarnings, len(logs));
continue;
}
}
}

func (this *DBTests) DBTestCourseSyncNewUsers(test *testing.T) {
defer ResetForTesting();

Expand Down
8 changes: 8 additions & 0 deletions model/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ func GetRoleString(role UserRole) string {
return roleToString[role];
}

func GetAllRoles() map[UserRole]string {
return roleToString;
}

func GetAllRoleStrings() map[string]UserRole {
return stringToRole;
}

func (this UserRole) MarshalJSON() ([]byte, error) {
buffer := bytes.NewBufferString(`"`);
buffer.WriteString(roleToString[this]);
Expand Down
12 changes: 9 additions & 3 deletions task/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"

"github.com/edulinq/autograder/email"
"github.com/edulinq/autograder/db"
"github.com/edulinq/autograder/log"
"github.com/edulinq/autograder/model"
"github.com/edulinq/autograder/model/tasks"
Expand All @@ -26,19 +27,24 @@ func RunReportTask(course *model.Course, rawTask tasks.ScheduledTask) (bool, err
func RunReport(course *model.Course, to []string) error {
report, err := report.GetCourseScoringReport(course);
if (err != nil) {
return fmt.Errorf("Failed to get scoring report for course '%s': '%w'.", course.GetName(), err);
return fmt.Errorf("Failed to get scoring report for course '%s': '%w'.", course.GetID(), err);
}

html, err := report.ToHTML();
if (err != nil) {
return fmt.Errorf("Failed to generate HTML for scoring report for course '%s': '%w'.", course.GetName(), err);
return fmt.Errorf("Failed to generate HTML for scoring report for course '%s': '%w'.", course.GetID(), err);
}

subject := fmt.Sprintf("Autograder Scoring Report for %s", course.GetName());

to, err = db.ResolveUsers(course, to);
if (err != nil) {
return fmt.Errorf("Failed to resolve users for course '%s': '%w'.", course.GetID(), err);
}

err = email.Send(to, subject, html, true);
if (err != nil) {
return fmt.Errorf("Failed to send scoring report for course '%s': '%w'.", course.GetName(), err);
return fmt.Errorf("Failed to send scoring report for course '%s': '%w'.", course.GetID(), err);
}

log.Debug("Report completed sucessfully.", course, log.NewAttr("to", to));
Expand Down

0 comments on commit fb34694

Please sign in to comment.