/// <summary> /// Ensures that a question to add or update is in a valid state. /// </summary> public async Task <bool> ValidateQuestionAsync( Question question, IModelErrorCollection errors, string classroomName) { var existingQuestion = await _dbContext.Questions .Where(q => q.QuestionCategory.Classroom.Name == classroomName) .Where(q => q.Id == question.Id) .Include(q => q.QuestionCategory) .SingleOrDefaultAsync(); if (existingQuestion != null) { _dbContext.Entry(existingQuestion).State = EntityState.Detached; } var newQuestionCategory = await _dbContext.QuestionCategories .Include(qc => qc.Classroom) .SingleOrDefaultAsync ( category => category.Id == question.QuestionCategoryId ); if (newQuestionCategory.Classroom.Name != classroomName) { throw new InvalidOperationException( "Category of question is not in the given classroom."); } if (existingQuestion?.QuestionCategory?.RandomlySelectedQuestionId != null && question.QuestionCategoryId != existingQuestion?.QuestionCategoryId) { throw new InvalidOperationException ( "The category cannot be changed for a randomly selected question choice." ); } if (existingQuestion != null && existingQuestion?.QuestionCategory?.RandomlySelectedQuestionId == null && newQuestionCategory.RandomlySelectedQuestionId != null) { throw new InvalidOperationException ( "The category cannot be changed from a non-random-choice category " + "to a random choice category." ); } if (await _dbContext.Questions.AnyAsync( q => q.Id != question.Id && q.Name == question.Name && q.QuestionCategoryId == question.QuestionCategoryId)) { errors.AddError("Name", "Another question with that name already exists."); } return(!errors.HasErrors); }
/// <summary> /// Validates that an announcement is correctly configured. /// </summary> public bool ValidateAnnouncement( Classroom classroom, Announcement announcement, IModelErrorCollection modelErrors) { var sectionIds = announcement.Sections ?.Select(s => s.SectionId) ?.ToList() ?? new List <int>(); if (!sectionIds.Any()) { modelErrors.AddError ( "Sections", "At least one section must be included." ); return(false); } if (sectionIds.Distinct().Count() != sectionIds.Count) { modelErrors.AddError ( "Sections", "Duplicate sections are not permitted." ); return(false); } if (sectionIds .Intersect(classroom.Sections.Select(s => s.Id)) .Count() != sectionIds.Count) { modelErrors.AddError ( "Sections", "Invalid sections selected." ); return(false); } return(true); }
/// <summary> /// Registers a user. /// </summary> public async Task <RegisterNewUserResult> RegisterNewStudentAsync( string classroomName, string sectionName, StudentRegistration registration, string confirmationUrlBuilder, IModelErrorCollection errors) { var section = _dbContext.Sections .Where(s => s.Classroom.Name == classroomName) .Include(s => s.Classroom) .SingleOrDefault(s => s.Name == sectionName); if (section == null) { return(RegisterNewUserResult.SectionNotFound); } if (!section.AllowNewRegistrations) { return(RegisterNewUserResult.SectionNotOpen); } var user = await GetAndUpdateCurrentUserAsync(); if (user != null) { return(RegisterNewUserResult.AlreadyRegistered); } if (!await _gitHubUserClient.DoesUserExistAsync(registration.GitHubLogin)) { errors.AddError("GitHubLogin", "The GitHub username does not exist."); return(RegisterNewUserResult.Failed); } user = new User() { UniqueId = _identityProvider.CurrentIdentity.UniqueId, UserName = _identityProvider.CurrentIdentity.UserName, FirstName = registration.FirstName, LastName = _identityProvider.CurrentIdentity.LastName, EmailAddress = registration.EmailAddress, EmailConfirmationCode = GenerateEmailConfirmationCode(), EmailAddressConfirmed = false, GitHubLogin = registration.GitHubLogin, SuperUser = false }; var membership = await EnsureSectionMembershipAsync(user, section, SectionRole.Student); await EnsureUserInGithubOrgAsync(user, membership.ClassroomMembership); await SendUserInvitationMailAsync(user, confirmationUrlBuilder); _dbContext.Users.Add(user); await _dbContext.SaveChangesAsync(); return(RegisterNewUserResult.Success); }
/// <summary> /// Updates a checkpoint. /// </summary> private bool UpdateCheckpoint(Checkpoint checkpoint, IModelErrorCollection modelErrors) { if (checkpoint.SectionDates != null) { var sections = checkpoint.SectionDates.Select(d => d.SectionId).ToList(); if (sections.Distinct().Count() != sections.Count) { modelErrors.AddError("SectionDates", "You may only have one due date per section."); } } if (checkpoint.TestClasses != null) { var testClasses = checkpoint.TestClasses.Select(tc => tc.TestClassId).ToList(); if (testClasses.Distinct().Count() != testClasses.Count) { modelErrors.AddError("TestClasses", "You may only have one entry per test class."); } } if (modelErrors.HasErrors) { return(false); } _dbContext.RemoveUnwantedObjects ( _dbContext.CheckpointDates, checkpointDates => checkpointDates.Id, checkpointDates => checkpointDates.CheckpointId == checkpoint.Id, checkpoint.SectionDates ); _dbContext.RemoveUnwantedObjects ( _dbContext.CheckpointTestClasses, testClass => testClass.Id, testClass => testClass.CheckpointId == checkpoint.Id, checkpoint.TestClasses ); return(true); }
/// <summary> /// Called to register the first super-user. /// </summary> public async Task <RegisterNewUserResult> RegisterFirstSuperUserAsync( SuperUserRegistration registration, IModelErrorCollection errors) { if (await AnyRegisteredUsersAsync()) { return(RegisterNewUserResult.AlreadyRegistered); } if (registration.ActivationToken != _activationToken.Value) { errors.AddError("ActivationToken", "Incorrect activation token."); return(RegisterNewUserResult.Failed); } if (!await _gitHubUserClient.DoesUserExistAsync(registration.GitHubLogin)) { errors.AddError("GitHubLogin", "The GitHub username does not exist."); return(RegisterNewUserResult.Failed); } User user = new User() { UniqueId = _identityProvider.CurrentIdentity.UniqueId, UserName = _identityProvider.CurrentIdentity.UserName, FirstName = registration.FirstName, LastName = registration.LastName, EmailAddress = registration.EmailAddress, EmailAddressConfirmed = true, GitHubLogin = registration.GitHubLogin, SuperUser = true }; _dbContext.Users.Add(user); await _dbContext.SaveChangesAsync(); return(RegisterNewUserResult.Success); }
/// <summary> /// Ensures that there are no duplicate section recipients. /// </summary> private void EnsureNoDuplicateSectionRecipients( Section section, IModelErrorCollection errors) { if (section.SectionRecipients != null) { var cmIds = section.SectionRecipients .Select(d => d.ClassroomMembershipId) .ToList(); if (cmIds.Distinct().Count() != cmIds.Count) { errors.AddError ( "SectionRecipients", "Duplicate section recipients are not permitted." ); } } }
/// <summary> /// Ensures that there are no duplicate section gradebooks. /// </summary> private void EnsureNoDuplicateSectionGradebooks( Section section, IModelErrorCollection errors) { if (section.SectionGradebooks != null) { var classroomGradebookIds = section.SectionGradebooks .Select(d => d.ClassroomGradebookId) .ToList(); if (classroomGradebookIds.Distinct().Count() != classroomGradebookIds.Count) { errors.AddError ( "SectionGradebooks", "You may only have one section gradebook per classroom gradebook." ); } } }
/// <summary> /// Ensures that section recipients are class admins. /// </summary> private async Task EnsureSectionRecipientsAreClassAdmins( Section section, IModelErrorCollection errors) { var classroomMemberships = await _dbContext.ClassroomMemberships .Where(cm => cm.ClassroomId == section.ClassroomId) .Where(cm => cm.Role >= ClassroomRole.Admin) .ToListAsync(); var cmIds = classroomMemberships.Select(cm => cm.Id).ToHashSet(); if (section.SectionRecipients != null && section.SectionRecipients.Any(sr => !cmIds.Contains(sr.ClassroomMembershipId))) { errors.AddError ( "SectionRecipients", "All section recipients must be class admins." ); } }
/// <summary> /// Returns a mock question updater factory. /// </summary> private Mock <IQuestionUpdaterFactory> GetMockQuestionUpdaterFactory( bool isValid) { var updater = new Mock <IQuestionUpdater>(); IModelErrorCollection errors = null; var updaterFactory = new Mock <IQuestionUpdaterFactory>(); updaterFactory .Setup ( m => m.CreateQuestionUpdater ( It.IsNotNull <Question>(), It.IsNotNull <IModelErrorCollection>() ) ).Callback <Question, IModelErrorCollection> ( (_unused, modelErrors) => errors = modelErrors ).Returns(updater.Object); updater .Setup(m => m.UpdateQuestionAsync()) .Callback ( () => { if (!isValid) { errors.AddError("Error", "ErrorDescription"); } } ).Returns(Task.CompletedTask); return(updaterFactory); }
/// <summary> /// Registers a user. /// </summary> public async Task<RegisterNewUserResult> RegisterNewStudentAsync( string classroomName, string sectionName, StudentRegistration registration, string confirmationUrlBuilder, IModelErrorCollection errors) { var section = _dbContext.Sections .Where(s => s.Classroom.Name == classroomName) .Include(s => s.Classroom) .SingleOrDefault(s => s.Name == sectionName); if (section == null) { return RegisterNewUserResult.SectionNotFound; } if (!section.AllowNewRegistrations) { return RegisterNewUserResult.SectionNotOpen; } var user = await GetAndUpdateCurrentUserAsync(); if (user != null) { return RegisterNewUserResult.AlreadyRegistered; } if (!await _gitHubUserClient.DoesUserExistAsync(registration.GitHubLogin)) { errors.AddError("GitHubLogin", "The GitHub username does not exist."); return RegisterNewUserResult.Failed; } user = new User() { UniqueId = _identityProvider.CurrentIdentity.UniqueId, UserName = _identityProvider.CurrentIdentity.UserName, FirstName = registration.FirstName, LastName = _identityProvider.CurrentIdentity.LastName, EmailAddress = registration.EmailAddress, EmailConfirmationCode = GenerateEmailConfirmationCode(), EmailAddressConfirmed = false, GitHubLogin = registration.GitHubLogin, SuperUser = false }; var membership = await EnsureSectionMembershipAsync(user, section, SectionRole.Student); await EnsureUserInGithubOrgAsync(user, membership.ClassroomMembership); await SendUserInvitationMailAsync(user, confirmationUrlBuilder); _dbContext.Users.Add(user); await _dbContext.SaveChangesAsync(); return RegisterNewUserResult.Success; }
/// <summary> /// Called to register the first super-user. /// </summary> public async Task<RegisterNewUserResult> RegisterFirstSuperUserAsync( SuperUserRegistration registration, IModelErrorCollection errors) { if (await AnyRegisteredUsersAsync()) { return RegisterNewUserResult.AlreadyRegistered; } if (registration.ActivationToken != _activationToken.Value) { errors.AddError("ActivationToken", "Incorrect activation token."); return RegisterNewUserResult.Failed; } if (!await _gitHubUserClient.DoesUserExistAsync(registration.GitHubLogin)) { errors.AddError("GitHubLogin", "The GitHub username does not exist."); return RegisterNewUserResult.Failed; } User user = new User() { UniqueId = _identityProvider.CurrentIdentity.UniqueId, UserName = _identityProvider.CurrentIdentity.UserName, FirstName = registration.FirstName, LastName = registration.LastName, EmailAddress = registration.EmailAddress, EmailAddressConfirmed = true, GitHubLogin = registration.GitHubLogin, SuperUser = true }; _dbContext.Users.Add(user); await _dbContext.SaveChangesAsync(); return RegisterNewUserResult.Success; }
/// <summary> /// Updates the given user. /// </summary> public async Task<bool> UpdateUserAsync( User user, string confirmationUrlBuilder, IModelErrorCollection modelErrors) { var existingUser = await _dbContext.Users .Include(u => u.ClassroomMemberships) .ThenInclude(cm => cm.Classroom) .SingleAsync(u => u.Id == user.Id); bool updatedEmail = false; if (user.GitHubLogin != existingUser.GitHubLogin) { if (!await _gitHubUserClient.DoesUserExistAsync(user.GitHubLogin)) { modelErrors.AddError("GitHubLogin", "The GitHub username does not exist."); return false; } if (existingUser.ClassroomMemberships != null) { foreach (var membership in existingUser.ClassroomMemberships) { var orgName = membership.Classroom.GitHubOrganization; var team = await _gitHubTeamClient.GetTeamAsync ( orgName, membership.GitHubTeam ); await _gitHubTeamClient.InviteUserToTeamAsync ( orgName, team, user.GitHubLogin ); await _gitHubTeamClient.RemoveUserFromTeamAsync ( orgName, team, existingUser.GitHubLogin ); membership.InGitHubOrganization = false; } } existingUser.GitHubLogin = user.GitHubLogin; } if (user.EmailAddress != existingUser.EmailAddress) { existingUser.EmailAddress = user.EmailAddress; existingUser.EmailAddressConfirmed = false; existingUser.EmailConfirmationCode = GenerateEmailConfirmationCode(); updatedEmail = true; } await _dbContext.SaveChangesAsync(); if (updatedEmail) { await SendUserInvitationMailAsync(existingUser, confirmationUrlBuilder); } return true; }
/// <summary> /// Updates the given user. /// </summary> public async Task <bool> UpdateUserAsync( User user, string confirmationUrlBuilder, IModelErrorCollection modelErrors) { var existingUser = await _dbContext.Users .Include(u => u.ClassroomMemberships) .ThenInclude(cm => cm.Classroom) .Include(u => u.AdditionalContacts) .SingleAsync(u => u.Id == user.Id); bool updatedEmail = false; if (user.GitHubLogin != existingUser.GitHubLogin) { if (!await _gitHubUserClient.DoesUserExistAsync(user.GitHubLogin)) { modelErrors.AddError("GitHubLogin", "The GitHub username does not exist."); return(false); } if (existingUser.ClassroomMemberships != null) { foreach (var membership in existingUser.ClassroomMemberships) { var orgName = membership.Classroom.GitHubOrganization; var team = await _gitHubTeamClient.GetTeamAsync ( orgName, membership.GitHubTeam ); await _gitHubTeamClient.InviteUserToTeamAsync ( orgName, team, user.GitHubLogin ); await _gitHubTeamClient.RemoveUserFromTeamAsync ( orgName, team, existingUser.GitHubLogin ); membership.InGitHubOrganization = false; } } existingUser.GitHubLogin = user.GitHubLogin; } if (user.PublicName != existingUser.PublicName) { existingUser.PublicName = user.PublicName; } if (user.EmailAddress != existingUser.EmailAddress) { existingUser.EmailAddress = user.EmailAddress; existingUser.EmailAddressConfirmed = false; existingUser.EmailConfirmationCode = GenerateEmailConfirmationCode(); updatedEmail = true; } foreach (var additionalContact in existingUser.AdditionalContacts.ToList()) { var modifiedContact = user.AdditionalContacts ?.SingleOrDefault(ac => ac.Id == additionalContact.Id); if (modifiedContact != null) { additionalContact.LastName = modifiedContact.LastName; additionalContact.FirstName = modifiedContact.FirstName; additionalContact.EmailAddress = modifiedContact.EmailAddress; } else { existingUser.AdditionalContacts.Remove(additionalContact); } } if (user.AdditionalContacts != null) { foreach (var potentialNewContact in user.AdditionalContacts) { if (!existingUser.AdditionalContacts.Any(ac => ac.Id == potentialNewContact.Id)) { existingUser.AdditionalContacts.Add(potentialNewContact); } } } await _dbContext.SaveChangesAsync(); if (updatedEmail) { await SendUserInvitationMailAsync(existingUser, confirmationUrlBuilder); } return(true); }
/// <summary> /// Validates that an assignment is correctly configured. /// </summary> public async Task <bool> ValidateAssignmentAsync( Assignment assignment, IModelErrorCollection modelErrors) { if (assignment.DueDates != null) { var sections = assignment.DueDates.Select(d => d.SectionId).ToList(); if (sections.Distinct().Count() != sections.Count) { modelErrors.AddError("DueDates", "You may only have one due date per section."); return(false); } } var existingAssignmentQuestions = await _dbContext.AssignmentQuestions .Where(aq => aq.AssignmentId == assignment.Id) .ToListAsync(); foreach (var oldQuestion in assignment.Questions) { var conflicts = existingAssignmentQuestions .Any ( newQuestion => newQuestion.Id == oldQuestion.Id && newQuestion.QuestionId != oldQuestion.QuestionId ); if (conflicts) { modelErrors.AddError("Questions", "You may not modify an existing question."); return(false); } _dbContext.Entry(oldQuestion).State = EntityState.Detached; } var questionNames = assignment.Questions .Select(aq => aq.Name) .ToList(); if (questionNames.Distinct().Count() != questionNames.Count) { modelErrors.AddError("Questions", "No two questions may have the same name."); return(false); } if (assignment.CombinedSubmissions) { var newQuestionIds = new HashSet <int> ( assignment.Questions .Select(aq => aq.QuestionId) ); bool anyUnsupportedQuestions = await _dbContext.Questions .Where(q => newQuestionIds.Contains(q.Id)) .AnyAsync(q => q.UnsupportedSolver(QuestionSolverType.NonInteractive)); if (anyUnsupportedQuestions) { modelErrors.AddError ( "CombinedSubmissions", "Submissions may not be combined if the assignment contains any questions " + "that do not support non-interactive submissions (such as code questions)." ); return(false); } } if (assignment.CombinedSubmissions && assignment.AnswerInOrder) { modelErrors.AddError ( "AnswerInOrder", "The 'Answer In Order' option may not be selected when submissions are combined." ); return(false); } if (!assignment.CombinedSubmissions && assignment.OnlyShowCombinedScore) { modelErrors.AddError ( "OnlyShowCombinedScore", "The 'Only Show Combined Score' option may only be selected when submissions are combined." ); return(false); } return(true); }
/// <summary> /// Updates a checkpoint. /// </summary> private bool UpdateCheckpoint(Checkpoint checkpoint, IModelErrorCollection modelErrors) { if (checkpoint.SectionDates != null) { var sections = checkpoint.SectionDates.Select(d => d.SectionId).ToList(); if (sections.Distinct().Count() != sections.Count) { modelErrors.AddError("SectionDates", "You may only have one due date per section."); } } if (checkpoint.TestClasses != null) { var testClasses = checkpoint.TestClasses.Select(tc => tc.TestClassId).ToList(); if (testClasses.Distinct().Count() != testClasses.Count) { modelErrors.AddError("TestClasses", "You may only have one entry per test class."); } } if (modelErrors.HasErrors) { return false; } _dbContext.RemoveUnwantedObjects ( _dbContext.CheckpointDates, checkpointDates => checkpointDates.Id, checkpointDates => checkpointDates.CheckpointId == checkpoint.Id, checkpoint.SectionDates ); _dbContext.RemoveUnwantedObjects ( _dbContext.CheckpointTestClasses, testClass => testClass.Id, testClass => testClass.CheckpointId == checkpoint.Id, checkpoint.TestClasses ); return true; }
/// <summary> /// Updates a question. /// </summary> public async Task<bool> UpdateQuestionAsync( string classroomName, Question question, IModelErrorCollection errors) { var classroom = await LoadClassroomAsync(classroomName); var questionCategory = await _dbContext.QuestionCategories .SingleOrDefaultAsync ( category => category.Id == question.QuestionCategoryId ); if (questionCategory.ClassroomId != classroom.Id) { throw new InvalidOperationException( "Category of question is not in the given classroom."); } if (await _dbContext.Questions.AnyAsync( q => q.Id != question.Id && q.Name == question.Name && q.QuestionCategoryId == question.QuestionCategoryId)) { errors.AddError("Name", "Another question with that name already exists."); return false; } await _questionUpdaterFactory.CreateQuestionUpdater(question, errors) .UpdateQuestionAsync(); if (errors.HasErrors) { return false; } _dbContext.Update(question); await _dbContext.SaveChangesAsync(); return true; }
/// <summary> /// Updates a assignment. /// </summary> private bool UpdateAssignment(Assignment assignment, IModelErrorCollection modelErrors) { if (assignment.DueDates != null) { var sections = assignment.DueDates.Select(d => d.SectionId).ToList(); if (sections.Distinct().Count() != sections.Count) { modelErrors.AddError("DueDates", "You may only have one due date per section."); return false; } } UpdateQuestionOrder(assignment.Questions); _dbContext.RemoveUnwantedObjects ( _dbContext.AssignmentQuestions, question => question.Id, question => question.AssignmentId == assignment.Id, assignment.Questions ); _dbContext.RemoveUnwantedObjects ( _dbContext.AssignmentDueDates, dueDate => dueDate.Id, dueDate => dueDate.AssignmentId == assignment.Id, assignment.DueDates ); return true; }