CSC/ECE 517 Spring 2015 E1529 GLDS: Difference between revisions
No edit summary |
|||
(209 intermediate revisions by 4 users not shown) | |||
Line 1: | Line 1: | ||
<font size="5"><b>E1529. Extend the Email notification feature to scheduled tasks </b></font> | <font size="5"><b>E1529. Extend the Email notification feature to scheduled tasks </b></font> | ||
= | =Overview= | ||
==Introduction to Expertiza== | |||
[http://expertiza.ncsu.edu/ Expertiza] is a web application developed by Ruby on Rails framework. It serves as a peer review system for professors and students at NC State and some other colleges and universities<ref>[https://github.com/expertiza/expertiza Expertiza | [http://expertiza.ncsu.edu/ Expertiza] is a web application developed by Ruby on Rails framework. It serves as a peer review system for professors and students at NC State and some other colleges and universities<ref>[https://github.com/expertiza/expertiza Expertiza Github]</ref>. Students can submit different assignments and peer-review reusable learning objects (articles, code, web sites, etc). It is also a powerful tool for professor to manage courses and assignments and so on. The latest "Rails 4" branch of Expertiza, although combined with various enhancements from the past two years, is seriously broken, from data migration to key feature implementation. Part of the reason has been the design strategy and code changes by various teams. | ||
==Email notification feature to scheduled tasks== | |||
This is a feature that has already been partially implemented in Expertiza [https://github.com/expertiza/expertiza/pull/445 E1451] implemented both sychronous and asychoronous mailers. Sychronous Emails refer to the Emails sent immediatly after an event (e.g. when student receive a peer-review). Asychoronous Email we implemented by the Gem "delayed job” and when a task (asychronous Email) is added to the delayed queue, a count-down number of minutes needs to be specified.<br> | This is a feature that has already been partially implemented in Expertiza [https://github.com/expertiza/expertiza/pull/445 E1451] implemented both sychronous and asychoronous mailers. Sychronous Emails refer to the Emails sent immediatly after an event (e.g. when student receive a peer-review). Asychoronous Email we implemented by the Gem "delayed job” and when a task (asychronous Email) is added to the delayed queue, a count-down number of minutes needs to be specified.<br> | ||
Our team | |||
Our team aims to extend this project. This wiki page documents the problem analysis and provides a guideline for future development and enhancement. | |||
__TOC__ | __TOC__ | ||
== | =Scope= | ||
E1451. Create Mailers for All Email Messages [https://github.com/expertiza/expertiza/pull/445 github] [http://wiki.expertiza.ncsu.edu/index.php/CSC/ECE_517_Fall_2014/OSS_E1451_las wiki] [http://expertiza.ncsu.edu/submitted_content/download/28122?current_folder%5Bname%5D=%2Flocal%2Frails%2Fexpertiza%2Freleases%2F20140702225848%2Fpg_data%2Fefg%2Fcsc517%2Ff14%2F%2F18&download=E1451_Report.pdf report] (Merged in [https://docs.google.com/a/ncsu.edu/document/d/1aypHb5UOe-3nPfruNeh2HZjeT-wfv6f-ZsZg2jDMNkQ/edit | ==Project Scope== | ||
E1451. Create Mailers for All Email Messages [https://github.com/expertiza/expertiza/pull/445 github] [http://wiki.expertiza.ncsu.edu/index.php/CSC/ECE_517_Fall_2014/OSS_E1451_las wiki] [http://expertiza.ncsu.edu/submitted_content/download/28122?current_folder%5Bname%5D=%2Flocal%2Frails%2Fexpertiza%2Freleases%2F20140702225848%2Fpg_data%2Fefg%2Fcsc517%2Ff14%2F%2F18&download=E1451_Report.pdf report] (Merged in [https://docs.google.com/a/ncsu.edu/document/d/1aypHb5UOe-3nPfruNeh2HZjeT-wfv6f-ZsZg2jDMNkQ/edit E1483]) | |||
Extending this project, the new system should be capable of | |||
*If one task is added to the delayed job queue, the asychronous Email should be updated or deleted automatically. | |||
*Add UI to visualize for the task in delayed job queue, instructors should be able to view tasks related to the assignments they have created. | |||
*Keep log of the scheduled tasks when they are scheduled and done, record events in the same log as project [https://github.com/expertiza/expertiza/pull/462 E1478]. | |||
*[optional] support more scheduled task: | |||
**instructors are able to schedule the time to drop all the outstanding reviews (reviews which has not been started) | |||
**instructors are able to schedule a specific time to send Emails to all the assignment participants who are still not in any team to find or form one. | |||
**(new) instructor should be able to schedule a time to drop all the topics which are held by 1 person teams. | |||
*create tests to make sure the test coverage increases. | |||
==Files Involved== | |||
Mailers: | |||
*delayed_mailer.rb | |||
Models: | |||
*assignment_form.rb | |||
*due_date.rb | |||
*delayed_job.rb(new created) | |||
*scheduled_task.rb(new created) | |||
Views: | |||
*_due_dates.html.erb | |||
*_assignments_actions.html.erb | |||
* scheduled_tasks.erb(new created) | |||
Controllers: | |||
*assignments_controller.rb | |||
==Gems Related== | |||
*gem 'delayed_job_active_record' | |||
[https://github.com/collectiveidea/delayed_job Delayed:Job] encapsulates the common pattern of asynchronously executing longer tasks in the background<ref> [https://github.com/collectiveidea/delayed_job Delayed_job Gem]</ref>. It allows to support multiple backends for storing the job queue. By using 'delayed_job_active_record' gem, we can use delayed_job with Active Record. This Active Record backend requires a job table. | |||
After running rails generate delayed_job:active_record and rake db:migrate, a "delayed_jobs" table is created in our database. This gem integrates well with many [http://en.wikipedia.org/wiki/Relational_database_management_system RDBMS] backend such as MySQL. Using this gem, we can store all scheduled tasks queue into the "delayed_jobs" table. | |||
*gem 'paper_trail' | |||
PaperTrail allows to track changes of models' data, which is good for versioning. You can see how a model looked at any stage in its lifecycle, revert it to any version, and even undelete it after it's been destroyed<ref>[https://github.com/airblade/paper_trail Paper_trail Gem]</ref>. Using this gem, we can keep log of the scheduled tasks. | |||
=Standards= | |||
All our code follows the [https://docs.google.com/document/d/1qQD7fcypFk77nq7Jx7ZNyCNpLyt1oXKaq5G-W7zkV3k/edit global rules] on Ruby style. | |||
=Problem Analysis= | |||
== Time Issues== | |||
Initially, when we want to verify the email function, we found many time issues in this system. That blocks our work about email notification. Some related problems are explained and analyzed in this section. | |||
===Wrong displayed time=== | |||
Firstly, when we set a due time to test the mail features, we found the display of time is incorrect. | |||
In the “Assignment Edit”->“Due dates”, when we modify the “Date & time” of the deadline, the displayed time will be 4 hours ahead of the time we saved.<br> | |||
[[File:Wrong display of time.png |frame|center|Set due time]] | |||
[[File:Wrong display of time 2.png |frame|center|After we click the save button, we can see that all the time are 4 hours ahead of the set time]] | |||
[[File:database_deadline.png |frame|center|The deadline time is stored correct in the database]] | |||
After several tries, we found that every time when we click "save", the saved time is 4 hours earlier then the last saved time. However, the time in the database is alway correct as the time we save. After we analyze, we found the problem may lay in the line of 68 of <code>views/assignment/edit/_due_dates.html.erb</code> file. | |||
<pre>due_at = new Date(due_at.substr(0, 16)).format('yyyy/mm/dd HH:MM');</pre> | |||
When we declare a Data object, this Data() function will perform a time zone conversion automatically. Yet Rails’ default timezone stored in database is UTC. So the displayed time(local time, which is EDT) and time in database is different. | |||
===Different time zone=== | |||
In the previous version, when current time is compared with the time stored in database, they may apply to different time zones so that the result of comparison may be wrong.<br> | |||
When we delay a deadline of an assignment, for example, we set the deadline of metareview from2015/03/28 16:00 to 2015/04/04 20:00 and set the reminder hr to 16 hours. | |||
[[File:Delayassign.png |frame|center|Delay a time]] | |||
After click "save", the run time showed database is about 8:00 am. Since 20-16=4, the correct run time is 4:00 am. | |||
[[File:delaydatabase.png |frame|center|Time Stored in Database]] | |||
The reason why there is a four-hour time difference is generated from the file <code>application_form.rb</code>, method <code> find_min_from_now(due_at)</code>, and line 142 <code>time_in_min=((due_at - curr_time).to_i/60)</code>. <code> due_at </code> is the system time for Rails (UTC), while <code> curr_time </code> is the local time which is EDT. The two time zones have four-hour time difference.<br> | |||
===New deadline replace the old one=== | |||
This time, we add a new delay in Review section, we delay the deadline from 2015/03/22 21:00 to 2015/03/30 21:00, and set the remind hr to 8 hours. | |||
[[File:delayagain.png |frame|center|Delay a new deadline type]] | |||
However, the delayed_jobs database form is still the same, which means in one assignment there is only one reminder can work.<br> | |||
As a result, we conclude that only one due_time record associated with different period exists in the database: | |||
*In Expertiza, one assignment has several period, such as submission, review, metareview. Administrates are able to set and postpone due dates for each period separately. | |||
*In the previous version, when the instructor postpone the review deadline and then postpone the submit deadline, the submit deadline’s modification will replace the the review deadline’s modification in database. | |||
*In this case, if both of them need Email reminder, there will be no Email about the former modified deadline. | |||
*After our analysis, we found the due time object is identified with assignment_id, instead of deadline type id. | |||
==UI issues== | |||
In the previous version, there is no UI to display the reminders of assignments. There is no way for instructors to view or delete their delayed tasks related to an assignment. We are going to add a button under Assignment->edit, and add a view to display all delayed jobs derived from the delayed_jobs table in our database. | |||
=System Design= | |||
==Fix Time Issues== | |||
===Unified time zone=== | |||
We convert current time to the same time zone as the deadline stored in database. Then the result of comparison will be right so that it can be used for further usage. | |||
In <code>app/models/assignment_form.rb</code>, we modified <code>curr_time=DateTime.now.to_s(:db)</code> to <code>curr_time=DateTime.now.in_time_zone(zone='UTC').to_s(:db)</code> in the <code>find_min_from_now</code> method, to make sure when calculating the minutes from now, the time zone is right.<br> | |||
Also, to make sure the right display of time, when log in as a administrator, go to Profile and choose the Preferred Time Zone to be "(GMT-05:00) Eastern Time (US&Canada)". | |||
===Separate deadline of each topic period=== | |||
In each Assignment, each deadline type (E.g review, submit, metareview) should has its own Email reminder time in the database. New postponed deadline of one type can only replace the same deadline type in the same assignment id category. This bug has been fixed by TA. | |||
==Add UI to visualize for scheduled tasks== | |||
All the scheduled tasks are in the <code>delayed_jobs</code> table in database, so the system will read from that table to show all the tasks' information. A new page is created to show all of those scheduled tasks and there is also a delete button for instructor to delete any task. Instructor can view this page via a <code>view delayed jobs</code> button in <code> assignment->edit </code>.<br> | |||
The code in <code>app/models/assignment_form.rb</code> add the scheduled tasks into <code>delayed_jobs</code> table.<br> | |||
<pre> | |||
if diff>0 | |||
dj=DelayedJob.enqueue(ScheduledTask.new(@assignment.id, deadline_type, due_date.due_at.to_s(:db)), 1, diff.minutes.from_now) | |||
change_item_type(dj.id) | |||
due_date.update_attribute(:delayed_job_id, dj.id) | |||
# If the deadline type is review, add a delayed job to drop outstanding review | |||
if deadline_type == "review" | |||
dj = DelayedJob.enqueue(ScheduledTask.new(@assignment.id, "drop_outstanding_reviews", due_date.due_at.to_s(:db)), 1, diff.minutes.from_now) | |||
change_item_type(dj.id) | |||
end | |||
# If the deadline type is team_formation, add a delayed job to drop one member team | |||
if deadline_type == "team_formation" | |||
dj = DelayedJob.enqueue(ScheduledTask.new(@assignment.id, "drop_one_member_topics", due_date.due_at.to_s(:db)), 1, diff.minutes.from_now) | |||
change_item_type(dj.id) | |||
end | |||
end | |||
</pre> | |||
After adding to the delayed_jobs, the database will store each task's assignment's id, deadline type, due date, task run time etc.<br> | |||
[[File:delayedjobs.png |frame|center|the delayed jobs table in database]]<br> | |||
We create a file <code>scheduled_tasks.erb</code> in the path: <code>/views/assignments</code> to show the form. In this file, we use a <code>@scheduled_jobs = DelayedJob.all </code> to read all datas from the "delayed_jobs" table. <br> | |||
<pre> | |||
<% @assignment = Assignment.find(params[:id]) %> | |||
<h1>Scheduled tasks for <%= @assignment.name %> assignment</h1> | |||
<% if flash[:notice] %> | |||
<div class="flash_note"><%= flash_message :notice %></div> | |||
<% end %> | |||
<% @scheduled_jobs = DelayedJob.all %> | |||
<table class="general"> | |||
<th width="25%">Deadline type #</th> | |||
<th width="25%">Run time</th> | |||
<th width="25%">Due date</th> | |||
<th width="25 %">Action</th> | |||
<th width="25 %">Delete</th> | |||
<% i=1 %> | |||
<% for delayed_job in @scheduled_jobs %> | |||
<tr> | |||
<%if delayed_job.handler.include? "assignment_id: #{@assignment.id}"%> | |||
<% handler = delayed_job.handler.split()%> | |||
<td><%= handler[5] %></td> | |||
<td><%= delayed_job.run_at %></td> | |||
<td><%= handler[7][1..-1] + ":" + handler[8][0..-2]%></td> | |||
<%if handler[5] == "submission" %> | |||
<td><%= " "%></td> | |||
<%end%> | |||
<%if handler[5] == "review" %> | |||
<td><%= " "%></td> | |||
<%end%> | |||
<%if handler[5] == "metareview" %> | |||
<td><%= " "%></td> | |||
<%end%> | |||
<%if handler[5] == "team_formation" %> | |||
<td><%= "send email to ask students to form teams"%></td> | |||
<%end%> | |||
<%if handler[5] == "drop_outstanding_reviews" %> | |||
<td><%= "drop outstanding reviews and send emails to those students"%></td> | |||
<%end%> | |||
<%if handler[5] == "drop_one_member_topics" %> | |||
<td><%= "drop one member team topics and send emails to those students"%></td> | |||
<%end%> | |||
<td><%= link_to image_tag('delete_icon.png', :title => 'Delete'),{:controller => 'assignments', :action => 'delete_scheduled_task',:id => @assignment.id, :delayed_job_id => delayed_job.id}%></td> | |||
<% end %> | |||
</tr> | |||
<% i=i+1 %> | |||
<% end %> | |||
</table> | |||
<% session[:return_to] = request.url %> | |||
<br/><br/> | |||
<a href="javascript:history.back()">Back</a> | |||
</pre><br> | |||
The following two pictures show our UI works. <br> | |||
[[File:viewdelayedjobs.png |frame|center|add a button to display all scheduled tasks]]<br> | |||
[[File:showscheduledtask.png|frame|center|The UI to show all the scheduled tasks]]<br> | |||
==Keep log of the scheduled tasks== | |||
According to the project [https://github.com/expertiza/expertiza/pull/462 E1478], there is a paper_trail<ref name = "Paper_trail Gem"> https://github.com/airblade/paper_trail]</ref> gem which can keep log of the models' data. What we need to do is let it also keep track of our scheduled task data.<br> | |||
<code>has_paper_trail</code> need to be written into a model which has inherited from ActiveRecord::Base. In our project, <code>Delayed::Job</code> should be such model. However, it is written in the Gem file which we can not directly modify. Thus we create a new file named <code>delayed_job.rb</code> in <code>models</code> folder, make it inherit from <code>Delayed::Job</code>, and add <code>has_paper_trail</code> in it. It is like: <br> | |||
<pre> | |||
class DelayedJob < Delayed::Job | |||
has_paper_trail | |||
end | |||
</pre> | |||
When the <code>delayed_jobs</code> table changes, a <code>Delayed::Backend::ActiveRecord::Job</code> record is automatically created in the log. In <code>assignment_form.rb</code>, we define a method to change the item type displayed in the log. | |||
<pre> | |||
def change_item_type(delayed_job_id) | |||
log = Version.where(item_type: "Delayed::Backend::ActiveRecord::Job", item_id: delayed_job_id).first | |||
log.update_attribute(:item_type, "ScheduledTask") #Change the item type in the log | |||
end | |||
</pre> | |||
Then we call it in <code>add_to_delayed_queue</code> method of the same file. Every time when the <code>delayed_jobs</code> table has added or deleted or updated data, the log can keep track of them. The following picture shows what the log is after creating scheduled tasks. You can see a "Search log" under the "logout" button. <br> | |||
[[File:scheduledtasklog.png|frame|center|four scheduled tasks are added in log]]<br> | |||
==support more schedule tasks== | |||
In our system, we would have four scheduled time: submission, review, metareview and team_formation. When due time comes, the system takes corresponding actions. These scheduled actions are called scheduled tasks. | |||
[[File:Support more scheduled tasks.png|frame|center|Support more scheduled tasks]] | |||
===Drop outstanding reviews=== | |||
In this task, instructors should be able to schedule a time to drop all the outstanding reviews, which means the reviews which has not been started.<br> | |||
Firstly, we create a new deadline type called "drop_outstanding_reviews". When we set the review’s due date, add an item into “delayed job” queue at the same time( refer to "Add UI to visualize for scheduled tasks"). | |||
When the review is due, it will automatically call a method to find all the outstanding reviews (reviews which have not been started) in the database then delete them. <br> | |||
There are two tables in the database to store the information about the reviews: <code>responses</code> and <code>response_maps </code>.<br> | |||
<code>responses</code> is used to store all the submitted reviews while <code>response_maps</code> stores all the requested reviews, which means if one review has began it will not stored in <code>responses</code> table. Thus, we just need to find the review data in <code>response_maps</code> while not in <code>responses</code> for one assignment and delete them in database.<br> | |||
We add a method in <code>/models/scheduled_task.rb</code> named <code>drop_outstanding_reviews</code> to drop those reviews.<br> | |||
<pre> | |||
def drop_outstanding_reviews | |||
reviews = ResponseMap.where(reviewed_object_id: self.assignment_id) | |||
for review in reviews | |||
review_has_began = Response.where(map_id: review.id) | |||
if review_has_began.size.zero? | |||
review_to_drop = ResponseMap.where(id: review.id) | |||
review_to_drop.first.destroy | |||
end | |||
end | |||
end | |||
</pre> | |||
Then, add the following code in <code>perform</code> method of the same file to drop one specified assignment's outstanding reviews at the due time.<br> | |||
<pre> | |||
if(self.deadline_type == "drop_outstanding_reviews") | |||
drop_outstanding_reviews | |||
end | |||
</pre> | |||
The following screenshots show the function realization: | |||
[[File:Before dropping review.png|frame|center| Before outstanding review is dropped]] [[File:After dropping review.png|frame|center| After outstanding review is dropped]] | |||
We set a review deadline in "Assignment"->Edit->due date". For better test, we enforce the action run time is 1 minute later after setting a deadline and adding it to a delayed_job queue. It is achieved by setting <code>diff = 1</code> in <code>add_to_delayed_queue</code> method. | |||
In order to add into the delayed_job table in the database, we need to check "Apply penalty policy" and choose one policy type after setting a deadline. Then no matter which due time it is set, the action runs 1 minute later. | |||
To trigger the delayed_job table, we need to run: <code>rake jobs:work</code>. | |||
===Send team formation Emails=== | |||
In this task, the instructors are able to schedule a specific time to send Emails to all the assignment participants who are still not in any team to find or form one. | |||
We use a previously existed deadline type called "team_formation" and enable to set a team_formation's due date and a reminder under "Assignment->edit->due date". Then add an item into “delayed job” at the same time(refer to "Add UI to visualize for scheduled tasks"). When “delayed job” is triggered, it will automatically call a method to find all the assignment participants who are still not in any team in the database, and send Emails to them to find teammate. <br> | |||
The code below achieves this task. If the deadline type is team formation, we called <code>get_one_member_team</code> and <code>email_reminder</code> to find one member team and send email to remind them of team formation. | |||
<pre> | |||
if(self.deadline_type == "team_formation") | |||
assignment = Assignment.find(self.assignment_id) | |||
if(assignment.team_assignment?) | |||
emails = get_one_member_team | |||
email_reminder(emails, self.deadline_type) | |||
end | |||
end | |||
</pre> | |||
The following screenshots show the function realization: | |||
[[File:Set team_formation deadline.png|frame|center|Set team_formation due date]] | |||
[[File:Receive reminders for team_formation.png|frame|center|Receive a reminder for team_formation]] | |||
First, we can set a team_formation deadline under "Assignment->edit->due date" and add it into the "delayed_job" table by checking "Apply penalty policy". Then we run a <code>rake jobs:work</code> to trigger the delayed_job. | |||
===Drop one-member topics=== | |||
In this task, the instructor should be able to schedule a time to drop all the topics which are held by 1 person teams. Here, "drop the topic" means release the topic from the person, not delete the topic. | |||
We create a new deadline type called "drop_one_member_topics". When deadline type is team_formation, we add an item to the "delayed_job" queue( refer to "Add UI to visualize for scheduled tasks"). | |||
Then when time is due, the system will call a method to find all the topics which are held by 1 person teams in the database, then the professor can drop those topics.<br> | |||
The code below firstly select one member team by counting the number of team members, then check if the one member team has signed up a topic. If yes, drop the topic from the team. | |||
<pre> | |||
def drop_one_member_topics | |||
teams = TeamsUser.all.group(:team_id).count(:team_id) | |||
for team_id in teams.keys | |||
if teams[team_id] == 1 | |||
topic_to_drop = SignedUpUser.where(creator_id: team_id).first | |||
if topic_to_drop#check if the one-person-team has signed up a topic | |||
topic_to_drop.delete | |||
end | |||
end | |||
end | |||
end | |||
</pre> | |||
Then we call this method when deadline type is drop_one_member_topics. | |||
<pre> | |||
if(self.deadline_type == "drop_one_member_topics") | |||
assignment = Assignment.find(self.assignment_id) | |||
if(assignment.team_assignment?) | |||
drop_one_member_topics | |||
end | |||
end | |||
</pre> | |||
The following screenshots show the function realization: | |||
[[File:Before dropping one member topic.png|frame|center|Before topic is dropped from one member team]] | |||
[[File:After dropping one member topic.png|frame|center|After topic is dropped from one member team]] | |||
First, we can set a team_formation deadline under "Assignment->edit->due date" and add it into the "delayed_job" table by checking "Apply penalty policy". Then we run a <code>rake jobs:work</code> to trigger the delayed_job. | |||
=Tests= | |||
==Testing the features== | |||
You can refer to this [https://www.youtube.com/watch?v=d96A_9j2rng video] to test all the features manually. | |||
==Spec tests== | |||
In this project, our jobs involve two gems as mentioned above. They are "Delayed Job", which can insert a job into queue and execute it at a certain time, and "Paper Trail", to log actions like create, update and destroy for a model. We write rspec tests to make sure that the functionality works well. | |||
In "scheduled_task_spec.rb", we test the functionality that when a due date is created, then delayed jobs of certain deadline type will be created. We may take "team_formation" deadline as an example: | |||
<pre> | |||
describe 'Team formation deadline reminder email' do | |||
it 'should send reminder email for team formation deadline to reviewers ' do | |||
id = 2 | |||
@name = "user" | |||
due_at = DateTime.now.advance(:minutes => +2) | |||
due_at1 = Time.parse(due_at.to_s(:db)) | |||
curr_time=DateTime.now.to_s(:db) | |||
curr_time=Time.parse(curr_time) | |||
time_in_min=((due_at1 - curr_time).to_i/60) *60 | |||
Delayed::Job.delete_all | |||
Delayed::Job.count.should == 0 | |||
dj = Delayed::Job.enqueue(ScheduledTask.new(id, "team_formation", due_at), 1, time_in_min) | |||
Delayed::Job.count.should == 1 | |||
Delayed::Job.last.handler.should include("deadline_type: team_formation") | |||
dj2 = Delayed::Job.enqueue(ScheduledTask.new(id, "drop_one_member_topics", due_at), 1, time_in_min) | |||
Delayed::Job.count.should == 2 | |||
Delayed::Job.last.handler.should include("deadline_type: drop_one_member_topics") | |||
end | |||
end | |||
</pre> | |||
When team_formation deadline is created, there will be two delayed jobs created. One is reminder email and the other one is drop topics from students who don't have a team yet. | |||
For the functionality of keeping log of scheduled tasks, "has_paper_trail_spec.rb" file is used to make sure that when a delayed job instance is created, one log will be generated. | |||
<pre> | |||
require 'rails_helper' | |||
describe 'has_paper_trail' do | |||
it "will create Version record when create delayed jobs record" do | |||
PaperTrail.enabled =true | |||
for version in Version.all | |||
version.delete | |||
end | |||
Version.all.count.should == 0 | |||
@delayed_job = DelayedJob.new | |||
@delayed_job.id = 1 | |||
@delayed_job.priority = 1 | |||
@delayed_job.attempts = 0 | |||
@delayed_job.save | |||
Version.all.count.should == 1 | |||
end | |||
end | |||
</pre> | |||
= | =References= | ||
<references/> | <references/> |
Latest revision as of 21:47, 24 April 2015
E1529. Extend the Email notification feature to scheduled tasks
Overview
Introduction to Expertiza
Expertiza is a web application developed by Ruby on Rails framework. It serves as a peer review system for professors and students at NC State and some other colleges and universities<ref>Expertiza Github</ref>. Students can submit different assignments and peer-review reusable learning objects (articles, code, web sites, etc). It is also a powerful tool for professor to manage courses and assignments and so on. The latest "Rails 4" branch of Expertiza, although combined with various enhancements from the past two years, is seriously broken, from data migration to key feature implementation. Part of the reason has been the design strategy and code changes by various teams.
Email notification feature to scheduled tasks
This is a feature that has already been partially implemented in Expertiza E1451 implemented both sychronous and asychoronous mailers. Sychronous Emails refer to the Emails sent immediatly after an event (e.g. when student receive a peer-review). Asychoronous Email we implemented by the Gem "delayed job” and when a task (asychronous Email) is added to the delayed queue, a count-down number of minutes needs to be specified.
Our team aims to extend this project. This wiki page documents the problem analysis and provides a guideline for future development and enhancement.
Scope
Project Scope
E1451. Create Mailers for All Email Messages github wiki report (Merged in E1483)
Extending this project, the new system should be capable of
- If one task is added to the delayed job queue, the asychronous Email should be updated or deleted automatically.
- Add UI to visualize for the task in delayed job queue, instructors should be able to view tasks related to the assignments they have created.
- Keep log of the scheduled tasks when they are scheduled and done, record events in the same log as project E1478.
- [optional] support more scheduled task:
- instructors are able to schedule the time to drop all the outstanding reviews (reviews which has not been started)
- instructors are able to schedule a specific time to send Emails to all the assignment participants who are still not in any team to find or form one.
- (new) instructor should be able to schedule a time to drop all the topics which are held by 1 person teams.
- create tests to make sure the test coverage increases.
Files Involved
Mailers:
- delayed_mailer.rb
Models:
- assignment_form.rb
- due_date.rb
- delayed_job.rb(new created)
- scheduled_task.rb(new created)
Views:
- _due_dates.html.erb
- _assignments_actions.html.erb
- scheduled_tasks.erb(new created)
Controllers:
- assignments_controller.rb
Gems Related
- gem 'delayed_job_active_record'
Delayed:Job encapsulates the common pattern of asynchronously executing longer tasks in the background<ref> Delayed_job Gem</ref>. It allows to support multiple backends for storing the job queue. By using 'delayed_job_active_record' gem, we can use delayed_job with Active Record. This Active Record backend requires a job table.
After running rails generate delayed_job:active_record and rake db:migrate, a "delayed_jobs" table is created in our database. This gem integrates well with many RDBMS backend such as MySQL. Using this gem, we can store all scheduled tasks queue into the "delayed_jobs" table.
- gem 'paper_trail'
PaperTrail allows to track changes of models' data, which is good for versioning. You can see how a model looked at any stage in its lifecycle, revert it to any version, and even undelete it after it's been destroyed<ref>Paper_trail Gem</ref>. Using this gem, we can keep log of the scheduled tasks.
Standards
All our code follows the global rules on Ruby style.
Problem Analysis
Time Issues
Initially, when we want to verify the email function, we found many time issues in this system. That blocks our work about email notification. Some related problems are explained and analyzed in this section.
Wrong displayed time
Firstly, when we set a due time to test the mail features, we found the display of time is incorrect.
In the “Assignment Edit”->“Due dates”, when we modify the “Date & time” of the deadline, the displayed time will be 4 hours ahead of the time we saved.
After several tries, we found that every time when we click "save", the saved time is 4 hours earlier then the last saved time. However, the time in the database is alway correct as the time we save. After we analyze, we found the problem may lay in the line of 68 of views/assignment/edit/_due_dates.html.erb
file.
due_at = new Date(due_at.substr(0, 16)).format('yyyy/mm/dd HH:MM');
When we declare a Data object, this Data() function will perform a time zone conversion automatically. Yet Rails’ default timezone stored in database is UTC. So the displayed time(local time, which is EDT) and time in database is different.
Different time zone
In the previous version, when current time is compared with the time stored in database, they may apply to different time zones so that the result of comparison may be wrong.
When we delay a deadline of an assignment, for example, we set the deadline of metareview from2015/03/28 16:00 to 2015/04/04 20:00 and set the reminder hr to 16 hours.
After click "save", the run time showed database is about 8:00 am. Since 20-16=4, the correct run time is 4:00 am.
The reason why there is a four-hour time difference is generated from the file application_form.rb
, method find_min_from_now(due_at)
, and line 142 time_in_min=((due_at - curr_time).to_i/60)
. due_at
is the system time for Rails (UTC), while curr_time
is the local time which is EDT. The two time zones have four-hour time difference.
New deadline replace the old one
This time, we add a new delay in Review section, we delay the deadline from 2015/03/22 21:00 to 2015/03/30 21:00, and set the remind hr to 8 hours.
However, the delayed_jobs database form is still the same, which means in one assignment there is only one reminder can work.
As a result, we conclude that only one due_time record associated with different period exists in the database:
- In Expertiza, one assignment has several period, such as submission, review, metareview. Administrates are able to set and postpone due dates for each period separately.
- In the previous version, when the instructor postpone the review deadline and then postpone the submit deadline, the submit deadline’s modification will replace the the review deadline’s modification in database.
- In this case, if both of them need Email reminder, there will be no Email about the former modified deadline.
- After our analysis, we found the due time object is identified with assignment_id, instead of deadline type id.
UI issues
In the previous version, there is no UI to display the reminders of assignments. There is no way for instructors to view or delete their delayed tasks related to an assignment. We are going to add a button under Assignment->edit, and add a view to display all delayed jobs derived from the delayed_jobs table in our database.
System Design
Fix Time Issues
Unified time zone
We convert current time to the same time zone as the deadline stored in database. Then the result of comparison will be right so that it can be used for further usage.
In app/models/assignment_form.rb
, we modified curr_time=DateTime.now.to_s(:db)
to curr_time=DateTime.now.in_time_zone(zone='UTC').to_s(:db)
in the find_min_from_now
method, to make sure when calculating the minutes from now, the time zone is right.
Also, to make sure the right display of time, when log in as a administrator, go to Profile and choose the Preferred Time Zone to be "(GMT-05:00) Eastern Time (US&Canada)".
Separate deadline of each topic period
In each Assignment, each deadline type (E.g review, submit, metareview) should has its own Email reminder time in the database. New postponed deadline of one type can only replace the same deadline type in the same assignment id category. This bug has been fixed by TA.
Add UI to visualize for scheduled tasks
All the scheduled tasks are in the delayed_jobs
table in database, so the system will read from that table to show all the tasks' information. A new page is created to show all of those scheduled tasks and there is also a delete button for instructor to delete any task. Instructor can view this page via a view delayed jobs
button in assignment->edit
.
The code in app/models/assignment_form.rb
add the scheduled tasks into delayed_jobs
table.
if diff>0 dj=DelayedJob.enqueue(ScheduledTask.new(@assignment.id, deadline_type, due_date.due_at.to_s(:db)), 1, diff.minutes.from_now) change_item_type(dj.id) due_date.update_attribute(:delayed_job_id, dj.id) # If the deadline type is review, add a delayed job to drop outstanding review if deadline_type == "review" dj = DelayedJob.enqueue(ScheduledTask.new(@assignment.id, "drop_outstanding_reviews", due_date.due_at.to_s(:db)), 1, diff.minutes.from_now) change_item_type(dj.id) end # If the deadline type is team_formation, add a delayed job to drop one member team if deadline_type == "team_formation" dj = DelayedJob.enqueue(ScheduledTask.new(@assignment.id, "drop_one_member_topics", due_date.due_at.to_s(:db)), 1, diff.minutes.from_now) change_item_type(dj.id) end end
After adding to the delayed_jobs, the database will store each task's assignment's id, deadline type, due date, task run time etc.
We create a file scheduled_tasks.erb
in the path: /views/assignments
to show the form. In this file, we use a @scheduled_jobs = DelayedJob.all
to read all datas from the "delayed_jobs" table.
<% @assignment = Assignment.find(params[:id]) %> <h1>Scheduled tasks for <%= @assignment.name %> assignment</h1> <% if flash[:notice] %> <div class="flash_note"><%= flash_message :notice %></div> <% end %> <% @scheduled_jobs = DelayedJob.all %> <table class="general"> <th width="25%">Deadline type #</th> <th width="25%">Run time</th> <th width="25%">Due date</th> <th width="25 %">Action</th> <th width="25 %">Delete</th> <% i=1 %> <% for delayed_job in @scheduled_jobs %> <tr> <%if delayed_job.handler.include? "assignment_id: #{@assignment.id}"%> <% handler = delayed_job.handler.split()%> <td><%= handler[5] %></td> <td><%= delayed_job.run_at %></td> <td><%= handler[7][1..-1] + ":" + handler[8][0..-2]%></td> <%if handler[5] == "submission" %> <td><%= " "%></td> <%end%> <%if handler[5] == "review" %> <td><%= " "%></td> <%end%> <%if handler[5] == "metareview" %> <td><%= " "%></td> <%end%> <%if handler[5] == "team_formation" %> <td><%= "send email to ask students to form teams"%></td> <%end%> <%if handler[5] == "drop_outstanding_reviews" %> <td><%= "drop outstanding reviews and send emails to those students"%></td> <%end%> <%if handler[5] == "drop_one_member_topics" %> <td><%= "drop one member team topics and send emails to those students"%></td> <%end%> <td><%= link_to image_tag('delete_icon.png', :title => 'Delete'),{:controller => 'assignments', :action => 'delete_scheduled_task',:id => @assignment.id, :delayed_job_id => delayed_job.id}%></td> <% end %> </tr> <% i=i+1 %> <% end %> </table> <% session[:return_to] = request.url %> <br/><br/> <a href="javascript:history.back()">Back</a>
The following two pictures show our UI works.
Keep log of the scheduled tasks
According to the project E1478, there is a paper_trail<ref name = "Paper_trail Gem"> https://github.com/airblade/paper_trail]</ref> gem which can keep log of the models' data. What we need to do is let it also keep track of our scheduled task data.
has_paper_trail
need to be written into a model which has inherited from ActiveRecord::Base. In our project, Delayed::Job
should be such model. However, it is written in the Gem file which we can not directly modify. Thus we create a new file named delayed_job.rb
in models
folder, make it inherit from Delayed::Job
, and add has_paper_trail
in it. It is like:
class DelayedJob < Delayed::Job has_paper_trail end
When the delayed_jobs
table changes, a Delayed::Backend::ActiveRecord::Job
record is automatically created in the log. In assignment_form.rb
, we define a method to change the item type displayed in the log.
def change_item_type(delayed_job_id) log = Version.where(item_type: "Delayed::Backend::ActiveRecord::Job", item_id: delayed_job_id).first log.update_attribute(:item_type, "ScheduledTask") #Change the item type in the log end
Then we call it in add_to_delayed_queue
method of the same file. Every time when the delayed_jobs
table has added or deleted or updated data, the log can keep track of them. The following picture shows what the log is after creating scheduled tasks. You can see a "Search log" under the "logout" button.
support more schedule tasks
In our system, we would have four scheduled time: submission, review, metareview and team_formation. When due time comes, the system takes corresponding actions. These scheduled actions are called scheduled tasks.
Drop outstanding reviews
In this task, instructors should be able to schedule a time to drop all the outstanding reviews, which means the reviews which has not been started.
Firstly, we create a new deadline type called "drop_outstanding_reviews". When we set the review’s due date, add an item into “delayed job” queue at the same time( refer to "Add UI to visualize for scheduled tasks").
When the review is due, it will automatically call a method to find all the outstanding reviews (reviews which have not been started) in the database then delete them.
There are two tables in the database to store the information about the reviews: responses
and response_maps
.
responses
is used to store all the submitted reviews while response_maps
stores all the requested reviews, which means if one review has began it will not stored in responses
table. Thus, we just need to find the review data in response_maps
while not in responses
for one assignment and delete them in database.
We add a method in /models/scheduled_task.rb
named drop_outstanding_reviews
to drop those reviews.
def drop_outstanding_reviews reviews = ResponseMap.where(reviewed_object_id: self.assignment_id) for review in reviews review_has_began = Response.where(map_id: review.id) if review_has_began.size.zero? review_to_drop = ResponseMap.where(id: review.id) review_to_drop.first.destroy end end end
Then, add the following code in perform
method of the same file to drop one specified assignment's outstanding reviews at the due time.
if(self.deadline_type == "drop_outstanding_reviews") drop_outstanding_reviews end
The following screenshots show the function realization:
We set a review deadline in "Assignment"->Edit->due date". For better test, we enforce the action run time is 1 minute later after setting a deadline and adding it to a delayed_job queue. It is achieved by setting diff = 1
in add_to_delayed_queue
method.
In order to add into the delayed_job table in the database, we need to check "Apply penalty policy" and choose one policy type after setting a deadline. Then no matter which due time it is set, the action runs 1 minute later.
To trigger the delayed_job table, we need to run: rake jobs:work
.
Send team formation Emails
In this task, the instructors are able to schedule a specific time to send Emails to all the assignment participants who are still not in any team to find or form one.
We use a previously existed deadline type called "team_formation" and enable to set a team_formation's due date and a reminder under "Assignment->edit->due date". Then add an item into “delayed job” at the same time(refer to "Add UI to visualize for scheduled tasks"). When “delayed job” is triggered, it will automatically call a method to find all the assignment participants who are still not in any team in the database, and send Emails to them to find teammate.
The code below achieves this task. If the deadline type is team formation, we called get_one_member_team
and email_reminder
to find one member team and send email to remind them of team formation.
if(self.deadline_type == "team_formation") assignment = Assignment.find(self.assignment_id) if(assignment.team_assignment?) emails = get_one_member_team email_reminder(emails, self.deadline_type) end end
The following screenshots show the function realization:
First, we can set a team_formation deadline under "Assignment->edit->due date" and add it into the "delayed_job" table by checking "Apply penalty policy". Then we run a rake jobs:work
to trigger the delayed_job.
Drop one-member topics
In this task, the instructor should be able to schedule a time to drop all the topics which are held by 1 person teams. Here, "drop the topic" means release the topic from the person, not delete the topic.
We create a new deadline type called "drop_one_member_topics". When deadline type is team_formation, we add an item to the "delayed_job" queue( refer to "Add UI to visualize for scheduled tasks").
Then when time is due, the system will call a method to find all the topics which are held by 1 person teams in the database, then the professor can drop those topics.
The code below firstly select one member team by counting the number of team members, then check if the one member team has signed up a topic. If yes, drop the topic from the team.
def drop_one_member_topics teams = TeamsUser.all.group(:team_id).count(:team_id) for team_id in teams.keys if teams[team_id] == 1 topic_to_drop = SignedUpUser.where(creator_id: team_id).first if topic_to_drop#check if the one-person-team has signed up a topic topic_to_drop.delete end end end end
Then we call this method when deadline type is drop_one_member_topics.
if(self.deadline_type == "drop_one_member_topics") assignment = Assignment.find(self.assignment_id) if(assignment.team_assignment?) drop_one_member_topics end end
The following screenshots show the function realization:
First, we can set a team_formation deadline under "Assignment->edit->due date" and add it into the "delayed_job" table by checking "Apply penalty policy". Then we run a rake jobs:work
to trigger the delayed_job.
Tests
Testing the features
You can refer to this video to test all the features manually.
Spec tests
In this project, our jobs involve two gems as mentioned above. They are "Delayed Job", which can insert a job into queue and execute it at a certain time, and "Paper Trail", to log actions like create, update and destroy for a model. We write rspec tests to make sure that the functionality works well. In "scheduled_task_spec.rb", we test the functionality that when a due date is created, then delayed jobs of certain deadline type will be created. We may take "team_formation" deadline as an example:
describe 'Team formation deadline reminder email' do it 'should send reminder email for team formation deadline to reviewers ' do id = 2 @name = "user" due_at = DateTime.now.advance(:minutes => +2) due_at1 = Time.parse(due_at.to_s(:db)) curr_time=DateTime.now.to_s(:db) curr_time=Time.parse(curr_time) time_in_min=((due_at1 - curr_time).to_i/60) *60 Delayed::Job.delete_all Delayed::Job.count.should == 0 dj = Delayed::Job.enqueue(ScheduledTask.new(id, "team_formation", due_at), 1, time_in_min) Delayed::Job.count.should == 1 Delayed::Job.last.handler.should include("deadline_type: team_formation") dj2 = Delayed::Job.enqueue(ScheduledTask.new(id, "drop_one_member_topics", due_at), 1, time_in_min) Delayed::Job.count.should == 2 Delayed::Job.last.handler.should include("deadline_type: drop_one_member_topics") end end
When team_formation deadline is created, there will be two delayed jobs created. One is reminder email and the other one is drop topics from students who don't have a team yet. For the functionality of keeping log of scheduled tasks, "has_paper_trail_spec.rb" file is used to make sure that when a delayed job instance is created, one log will be generated.
require 'rails_helper' describe 'has_paper_trail' do it "will create Version record when create delayed jobs record" do PaperTrail.enabled =true for version in Version.all version.delete end Version.all.count.should == 0 @delayed_job = DelayedJob.new @delayed_job.id = 1 @delayed_job.priority = 1 @delayed_job.attempts = 0 @delayed_job.save Version.all.count.should == 1 end end
References
<references/>