CSC/ECE 517 Fall 2018/E1834 Improve email notifications
This page provides a description of the Expertiza based OSS project.
About Expertiza
Expertiza is an open source project developed using Ruby on Rails framework.Expertiza allows the instructor to create new assignments and customize new or existing assignments.The application allows students to submit and peer-review learning objects (articles, code, web sites, etc)[1].Expertiza supports submission across various document types, including the URLs and wiki pages.
Problem Statement
Approach Taken To Implement Changes
NOTE: All the mails except the ones for the reviewer which are sent to mailinator are sent to expertiza.development@gmail.com ,as this is already set in the development environment.
1) Mail sent when participant added to assignment :
When students' accounts are created by importing a CSV file on the Users page,they receive e-mails but not when user was added as a participant to the assignment.We added a method in the assignment_participant.rb model to send mails when a participant is added to an assignment on the assignment page through a CSV file.This functionality is implemented with using the MailerHelper class.
# provide import functionality for Assignment Participants # if user does not exist, it will be created and added to this assignment def self.import(row_hash, _row_header = nil, session, id) raise ArgumentError, "No user id has been specified." if row_hash.empty? user = User.find_by(name: row_hash[:name]) if user.nil? raise ArgumentError, "The record containing #{row_hash[:name]} does not have enough items." if row_hash.length < 4 attributes = ImportFileHelper.define_attributes(row_hash) user = ImportFileHelper.create_new_user(attributes, session) end raise ImportError, "The assignment with id \"#{id.to_s}\" was not found." if Assignment.find(id).nil? unless AssignmentParticipant.exists?(user_id: user.id, parent_id: id) new_part = AssignmentParticipant.create(user_id: user.id, parent_id: id) new_part.set_handle end prepared_mail = MailerHelper.send_mail_to_user(user, "Your Expertiza account and password have been created.", "user_welcome", "password") prepared_mail.deliver end
We have in a similar fashion added a method in the course_participant model.
# provide import functionality for Course Participants # if user does not exist, it will be created and added to this assignment def self.import(row_hash, _row_header = nil, session, id) raise ArgumentError, "No user id has been specified." if row_hash.empty? user = User.find_by(name: row_hash[:name]) if user.nil? raise ArgumentError, "The record containing #{row_hash[:name]} does not have enough items." if row_hash.length < 4 attributes = ImportFileHelper.define_attributes(row_hash) user = ImportFileHelper.create_new_user(attributes, session) end course = Course.find_by(id) raise ImportError, "The course with the id \"" + id.to_s + "\" was not found." if course.nil? unless CourseParticipant.exists?(user_id: user.id, parent_id: id) CourseParticipant.create(user_id: user.id, parent_id: id) end password = "password"#user.password prepared_mail = MailerHelper.send_mail_to_user(user, "Your Expertiza account and password have been created.", "user_welcome", password) prepared_mail.deliver end
2) Sending email to reviewer when new submission is availble:
Added functionality to send email to the reviewer when new submission is available by making changes in the submitted_content_controller and assignment_participant model. The method handled boundary constraints like checking whether the round was valid and disabling email notification after the last round of review.
#assignment_participant.rb def is_in_final_round? topic_id = SignedUpTeam.topic_id(self.parent_id, self.user_id) return false if topic_id.nil? and self.staggered_deadline? next_due_date = DueDate.get_next_due_date(self.parent_id, topic_id) return !!(next_due_date.round == assignment.num_review_rounds && next_due_date.deadline_type_id == 2) end
Extra functionality of specifying the current review round and providing the direct link to reviewer in the mail itself also implemented in the submitted_content controller using the ReviewResponseMap and Response class.
#submitted_content_controller. # Send email to reviewers to review new submission, if review_round is valid and not last. @participant = AssignmentParticipant.find(params[:id]) # Send email to reviewers to review new submission, if the participant is not in the last round. if !@participant.is_in_final_round? @participant.reviewers.each do |reviewer| map = ReviewResponseMap.where(['reviewer_id = ? and reviewee_id = ?', reviewer.id, @participant.team.id]).first responses = Response.where(:map_id => map.id) responses = responses.sort_by { |obj| obj.updated_at }
# the latest response will be the last latest_response = responses.last
# we need to pass the id of lastest_response in the URL mentioned in the mail. # this will open the correct /response/edit?id=#{latest_response.id} page for the reviewer when (s)he clicks on it. Mailer.delayed_message(bcc: [User.find(reviewer.user_id).email], subject: "You have a new submission to review", body: "Please visit https://expertiza.ncsu.edu/response/edit?id=#{latest_response.id} and proceed to peer reviews.").deliver_now end end
3) Including a specific link for the deadline reminders email functionality :
Added a review_reminder_email method and mail_reviewers method in the delayed_mailer.rb file which implemented the functionality for sending deadline reminder mails which includes a link on where to gon to perform the specific task.
#delayed_mailer.rb def mail_reviewers email_list = [] reviews = ReviewResponseMap.where(reviewed_object_id: self.assignment_id) reviews.each do |review| participant = Participant.where(parent_id: self.assignment_id, id: review.reviewer_id).first email_list << {'email' => participant.user.email, 'participant_id' => participant.id} end review_reminder_email(email_list, self.deadline_type) unless email_list.empty? end
def review_reminder_email(email_list, deadline_type) assignment = Assignment.find(self.assignment_id) subject = "Message regarding #{deadline_type} for assignment #{assignment.name}" for item in email_list body = "This is a reminder to complete #{deadline_type} for assignment #{assignment.name}. \ Deadline is #{self.due_at}. Please visit https://expertiza.ncsu.edu/student_review/list?id=#{item.participant_id}\ If you have already done the #{deadline_type}, please ignore this mail." Rails.logger.info item.email @mail = Mailer.delayed_message(bcc: [item.email], subject: subject, body: body) @mail.deliver_now end
@count += 1 if @count % 3 == 0 if assignment.instructor.copy_of_emails # if email is sent to instructor, notice that the link in that email will contain the '?id=' field of the last participant @mail = Mailer.delayed_message(bcc: [assignment.instructor.email], subject: subject, body: body) @mail.deliver_now end end end
Corresponding changes in the background_email_reminder.rake file implemented. A method named send_reminder_mails implemented.
def send_reminder_emails(email_list, assign_name, due_date, assign_type) due_date_string = due_date.due_at.to_s subject = "Message regarding #{assign_type} for #{assign_name}" for item in email_list body = "This is a reminder to complete #{assign_type} for assignment #{assign_name}. " + "Deadline is #{due_date_string}. " + "Please visit https://expertiza.ncsu.edu/response/edit?id=#{item.response_id}" Mailer.deliver_message({ :bcc => [item.email], :subject => subject, :body => body }) end end
Testing
We have used Rspec for testing the email functionalities.Using the test driven development(TDD) approach, we have added an Rspec test which checks whether the mail is delivered to the expected receiver upon creation of the account for the student. We have created a new assignment_particpant_ spec file for the above purpose.The spec uses double and stub features of rspec gem to fake user login.
Since the smtp setting are set to test mode all mails go to expertiza.development@gmail.com and not the actual user.Hence when an email is sent the ActionMailer::Base.deliveries list gets updated.
context "when participant is added to assignment using csv" do let(:row) do {name: 'no one', fullname: 'no one', email: 'name@email.com', role:'user_role_name', parent: 'user_parent_name'} end let(:attributes) do {role_id: 1, name: 'no one', fullname: 'no one', email: 'name@email.com', email_on_submission: 'name@email.com', email_on_review: 'name@email.com', email_on_review_of_review: 'name@email.com'} end it "should send an email to the participant", :mailer => true do allow(ImportFileHelper).to receive(:define_attributes).with(row).and_return(attributes) allow(ImportFileHelper).to receive(:create_new_user).with(attributes, {}).and_return(double('User', id: 1, email: 'name@email.com', first_name: 'no name', last_name: 'no name', name: 'no name')) allow(Assignment).to receive(:find).with(1).and_return(assignment) allow(AssignmentParticipant).to receive(:exists?).with(user_id: 1, parent_id: 1).and_return(false) allow(AssignmentParticipant).to receive(:create).with(user_id: 1, parent_id: 1).and_return(participant) allow(participant).to receive(:set_handle).and_return('handle') # Check that ActionMailer::Base.deliveries list is updated when email is set by import method. # The last element in the deliveries list will be the email that was just sent when import method was invoked # We need to verify the email was correct by checking its subject, from and to. # In development/test mode, all mails go to expertiza.development@gmail.com and not actual user.to expect { AssignmentParticipant.import(row, nil, {}, 1) }.to change { ActionMailer::Base.deliveries.count }.by(1) email = ActionMailer::Base.deliveries.last expect(email.from[0]).to eq('expertiza.development@gmail.com') expect(email.to[0]).to eq('expertiza.development@gmail.com') expect(email.subject).to eq('Your Expertiza account and password have been created.') end end