CSC/ECE 517 Fall 2017/E1781 Topic Management: Difference between revisions

From Expertiza_Wiki
Jump to navigation Jump to search
 
(60 intermediate revisions by 3 users not shown)
Line 5: Line 5:
* Instructor Login: '''username:''' ''instructor6'' '''password:''' ''password''
* Instructor Login: '''username:''' ''instructor6'' '''password:''' ''password''


This project does compile.  Reviewers can refer [[#Testing Plan: Testing from the UI]](below) and the YouTube link for navigation help.
This project does compile.  Reviewers can refer [[#Testing Plan: Testing from the UI|Testing Plan: Testing from the UI]] and the YouTube link https://youtu.be/fCxhD-qFUro for navigation help.
 
 
The testing plan is at the end of this document.  Design principles were not needed since we mostly modified existing work.


== Introduction ==
== Introduction ==
=== Background ===
=== Background ===
Expertiza is a web portal which can be used to manage assignments related to a course. It provides a platform to view assignments, manage teams, select topics and work improvement through anonymous peer reviews. For the instructor it provides complete control to create assignments, view reviews submitted and provide feedback. The instructors also have an option to publish the students work based on the rights provided by the student.
Expertiza is a web portal which can be used to manage assignments related to a course. It provides a platform to view assignments, manage teams, select topics and work on improvement through anonymous peer reviews.


== Problem Statement ==
=== Problem Statement ===
The following were the tasks identified to be accomplished through this project.  
Expertiza allows the instructor to define different topics that students or teams could choose from as their assignment. Each topic can have 0 or more slots that indicate the number of students or teams that can be signed up for that topic.
To improve the user experience when managing topics, the following tasks were identified:
* '''Issue #971:''' Change create topic UI into AJAX  
* '''Issue #971:''' Change create topic UI into AJAX  
* '''Issue #926:''' We need a way to sort topics by topic number in assignment#edit page
* '''Issue #926:''' We need a way to sort topics by topic number in assignment#edit page
* '''Issue #718:''' We should allow instructors to give feedback when accepting or rejecting topic suggestions
* '''Issue #718:''' We should allow instructors to give feedback when accepting or rejecting topic suggestions


== Files Modified in this Project ==
== Files Modified ==
The following files were modified for this project.
The following files were modified for this project:
* app/assets/javascripts/application.js
* app/assets/javascripts/application.js
* app/assets/javascripts/signup.js
* app/assets/javascripts/signup.js
Line 33: Line 31:
* bower.json
* bower.json
* config/routes.rb
* config/routes.rb
* Gemfile - changed json from version 1.8.3 to 1.8.5
== Files Added ==
The following files were added to this project:
* app/assets/images/icons-2x.png
* app/assets/images/icons.png


== Solutions implemented and Delivered ==
== Solutions implemented and Delivered ==
=== Issue #971: ===
=== Issue #971: Change create topic UI into AJAX===
This task was pretty involved and called for integrating an institution_id into the course table and updating the Course pages to specify the institution. We modified several parts of the program in order to accomplish this task.
Currently, when instructors manually enter topics, they have to go back and forth between the list of the topic page (views>sign_up_sheet>_add_signup_topics.html.erb) and the create topic page(views>sign_up_sheet>new.html.erb). Adding a new topic can be done through an editable grid (see js-grid) without leaving the list of topic page, by making use of AJAX. The list should be automatically updated when a new topic is entered. To enable AJAX the sign_up_sheet_controller.rb is modified so that it renders JSON to handle the javascript table/form.  


In addition, when adding a topic, the default slot is set as 1 instead of 0. The current warning message that shows up when the slot is 0, has been fixed to close properly.


On the controller '''''app/controllers/course_controller''''' we edited the update definition and the create definition to store the institution_id into the course table.
On the controller '''''app/controllers/sign_up_sheet_controller.rb''''' we edited the Load_add_signup_topics, which was converted from a function into a Rest endpoint(Json).


   def update
  # retrieves all the data associated with the given assignment. Includes all topics,
     @course = Course.find(params[:id])
  # this should retrieve results in Json so that it can be ajaxed
     if params[:course][:directory_path] and @course.directory_path != params[:course][:directory_path]
  # 1781
       begin
  # the following method is an action which renders all the topics of an assignment in the JSON format.
         FileHelper.delete_directory(@course)
   def load_add_signup_topics
       rescue
     @id = params[:id]
         flash[:error] = $ERROR_INFO
     assignment_id =  params[:id]
    @sign_up_topics = SignUpTopic.where('assignment_id = ?', assignment_id)
    @slots_filled = SignUpTopic.find_slots_filled(assignment_id)
    @slots_waitlisted = SignUpTopic.find_slots_waitlisted(assignment_id)
    @assignment = Assignment.find(assignment_id)
    @participants = SignedUpTeam.find_team_participants(assignment_id)
    # colloborating for JSON
    @sign_up_topics.each {|topic|
      topic_id = topic.id
      slots_fill_temp = 0
      slots_waitlisted = 0
      participants = []
       if @slots_filled
         @slots_filled.each {|slot|
          if slot.topic_id == topic_id
            slots_fill_temp = slot.count
          end
        }
      end
       if @slots_waitlisted
         @slots_waitlisted.each {|slot|
          if slot.topic_id == topic_id
            slots_waitlisted = slot.count
          end
        }
       end
       end
       begin
       if @participants
         FileHelper.create_directory_from_path(params[:course][:directory_path])
         @participants.each {|participant|
      rescue
          if participant.topic_id == topic_id
         flash[:error] = $ERROR_INFO
 
            participants << participant
          end
         }
       end
       end
    end
      topic.slots_filled_value = slots_fill_temp
    @course.name = params[:course][:name]
      topic.slots_waitlisted = slots_waitlisted
    '''@course.institutions_id = params[:course][:institutions_id]'''
      topic.slots_available = topic.max_choosers - topic.slots_filled_value
    @course.directory_path = params[:course][:directory_path]
      topic.partipants = participants
    @course.info = params[:course][:info]
     }
     @course.save
     # ACS Removed the if condition (and corresponding else) which differentiate assignments as team and individual assignments
     undo_link("The course \"#{@course.name}\" has been updated successfully.")
    # to treat all assignments as team assignments
     redirect_to controller: 'tree_display', action: 'list'
    # Though called participants, @participants are actually records in signed_up_teams table, which
  end
    # is a mapping table between teams and topics (waitlisted recored are also counted)
  def create
     render :json => {
    @course = Course.new(name: params[:course][:name], '''institutions_id: params[:course][:institutions_id],''' directory_path: params[:course][:directory_path], info: params[:course][:info], private: params[:course][:private])
      :id => @id.as_json,
      :sign_up_topics => @sign_up_topics.as_json( :methods => [:slots_filled_value,:slots_waitlisted,:slots_available,:partipants]),
      :slots_waitlisted => @slots_waitlisted.as_json,
      :assignment => @assignment.as_json
    }
   end
   end


An Example output of the Json rendered for Assignment Id : 843 by the load_add_signup_topics Action is as follows:


On the model '''''app/models/course.rb''''' we created a relationship where course belongs to institution. This is to allow the linkage of the two tables and allow the drop down to work in the view.
{
"id": "843",
"sign_up_topics": [
{
"id": 3958,
"topic_name": "Power consumption issue",
"assignment_id": 843,
"max_choosers": 3,
"category": "",
"topic_identifier": "1.1.1",
"micropayment": 0,
"private_to": null,
"description": null,
"link": null,
"slots_filled_value": 0,
"slots_waitlisted": 0,
"slots_available": 3,
"partipants": []
},
{
"id": 3959,
"topic_name": "Perspectives on parallel computers (1.4 in old edition)",
"assignment_id": 843,
"max_choosers": 2,
"category": "",
... (And so on.)


  belongs_to :institution, class_name: 'Institution', foreign_key: 'institutions_id'
Similarly, all the actions/methods (destroy|setup_new_topic|update_existing_topic|update|set_values_for_new_topic ) were changed to render Json rather than rendering a view, in order to satisty the Ajax requirements in the front end.


On the model '''''app/models/sign_up_topic.rb''''' an attribute accessor was created for variables to set properties that are required for the JSON.


On the model '''''app/models/institution.rb''''' we also created the relationship where institution has many courses.  This is also to allow linkage of the two tables for the drop down to work in the view.
  attr_accessor :slots_filled_value
  attr_accessor :slots_waitlisted
  attr_accessor :slots_available
  attr_accessor :partipants


  class Institution < ActiveRecord::Base
On the View ''''app/views/sign_up_sheet/_add_signup_topics.html.erb'''' the following was added:
    attr_accessible :name
    has_many :courses
    validates_length_of :name, minimum: 1
    validates_uniqueness_of :name
  end


<%= content_tag :div, id: "jsGrid", data: { assignmentid: @assignment.id }  do %>
<% end %>


On the view '''''app/views/course/_course.html.erb''''' we added the collection box to display the linked name from the institution table to the institution_id in the course table.  This completed the ability to select and store the institution_id into the database.
The content of the above tag is completely rendered via Javascript, where we use JSGrid for dynamically allowing users to Add, Delete and Update Topics on the same page.
<pre>
  <p><label for="institution_name">Institution Name</label><br/>
  <%= select("course", "institutions_id", Institution.all.collect{ |c| [ c.name, c.id] }) %></p>
</pre>


In '''' app/assets/javascripts/signup.js '''' file, the following was added:
The following content shows how AJAX calls are handled/made to our signup controller so that the CRUD operations on the topics page is dealt with


In order to properly display the institution_id associated with the courses on the view '''''app/views/tree_display/list.html.erb''''' it was necessary to understand the Factory Method design pattern. A subclass of '''''app/models/node.rb''''' is '''''app/models/course_node.rb''''' and '''''app/models/assigment_node.rb'''''.  Both need similar functions to be used by '''''app/assets/javascripts/tree_display.jsx''''' for displaying rows of courses and assignments.  This was added to both, but only used by '''''app/models/course_node.rb''''':
<pre>
 
/* controller Object : All the CRUD functionalities of our JS GRID is taken care under this controller object*/
  def get_institution_id
        controller: {
    # Course.find(self.node_object_id).course_id
            /*This makes an Ajax call to load all the signup topics which will be rendered as a json from endpoint */
    @course = Course.find(self.node_object_id) unless @course
            loadData: function(filter) {
    @course.institutions_id
                var data = $.Deferred();
  end
                $.ajax({
                    type: "GET",
                    contentType: "application/json; charset=utf-8",
                    url: "/sign_up_sheet/" + getAssignmentId() + "/load_add_signup_topics", // end point for making the load topics
                    dataType: "json"
                }).done(function(response) {
                    var sign_up_topics = response.sign_up_topics;
                    data.resolve(sign_up_topics);
                }).fail(function(response) {
                    alert("Issue on Loading Topics"); // If problem occurs in loading topics
                    data.resolve(response);
                });
                return data.promise();
            },


            /*This makes an Ajax call to insert a new  signup topic which will be entered by the user as topic argument */
            insertItem: function(topic) {
                topic.id = getAssignmentId() // the data to be sent through Ajax should be having the assignment id in id field
                var data = $.Deferred();
                $.ajax({
                    type: "POST",
                    url: "/sign_up_sheet/",
                    data: topic
                }).done(function(response) {
                    jQuery("#jsGrid").jsGrid("loadData");
                    data.resolve(response);
                }).fail(function(response) {
                    var responseJson = response;
                    data.resolve(response);
                    // this is the special case when user tries to set slots than slots already booked
                    if (responseJson.status == 400) {
                        jQuery(document).scrollTop(0);
                        location.reload();
                    } else
                        alert(responseJson.responseText);
                });
                return data.promise();
            },


'''''app/controllers/tree_display_controller.rb''''' was modified to include a local variable for used by the view in displaying the institution_id.
            /*This makes an Ajax call to update a single record in our JS Grid when user makes changes to it*/
            updateItem: function(topic) {
                var data = $.Deferred();
                $.ajax({
                    type: "PUT",
                    url: "/sign_up_sheet/" + topic.id,
                    data: topic
                }).done(function(response) {
                    jQuery("#jsGrid").jsGrid("loadData");
                    data.resolve(response);
                }).fail(function(response) {
                    var responseJson = response;
                    data.resolve(previousItem);
                    // this is the special case when user tries to set slots than slots already booked
                    if (responseJson.status == 400) {
                        jQuery(document).scrollTop(0);
                        location.reload();
                    } else
                        alert(responseJson.responseText);
                });
                return data.promise();
            },


  def update_tmp_obj(tmp_object, node)
            /*This makes an Ajax call to delete a single record*/
    tmp={
            deleteItem: function(topic) {
      "directory" => node.get_directory,
                return $.ajax({
      "creation_date" => node.get_creation_date,
                    type: "DELETE",
      "updated_date" => node.get_modified_date,
                    url: "/sign_up_sheet/" + topic.id
      "institution" => Institution.where(:id => node.get_institution_id),
                });
      "private" => node.get_instructor_id == session[:user].id ? true : false
            }
      }
        }
  tmp_object.merge!(tmp)
</pre>
  end


The Preview View for Topics Management :
[[file:Screenshot from 2017-10-27 17-32-46.png]]


'''''app/assets/javascript/tree_display.jsx''''' was also modified to show different rows between courses and assignments, since only courses were required to show the institution_id.  This was added in order to show the institution_id which is displayed on '''''app/views/tree_display/list.html.erb''''':
After Our Implementation using JS Grids :
[[file:newjsgrid.png]]


  jQuery.each(this.props.data, function (i, entry) {
=== Issue #926: ===
    if (((entry.name && entry.name.indexOf(_this.props.filterText) !== -1) ||
This task is to do with sorting the Topics according to the topic number.
      (entry.directory && entry.directory.indexOf(_this.props.filterText) !== -1) ||
This functionality is taken care by Js-Grid by itself, where clicking the topic# will toggle the topics in the ascending/descending order.
      (entry.creation_date && entry.creation_date.indexOf(_this.props.filterText) !== -1) ||
      (entry.instructor && entry.instructor.indexOf(_this.props.filterText) !== -1) ||
      '''(entry.institution && entry.institution.indexOf(_this.props.filterText) !== -1) ||'''
      (entry.updated_date && entry.updated_date.indexOf(_this.props.filterText) !== -1)) &&
      (entry.private == true || entry.type == 'FolderNode')) {
      _rows.push(<ContentTableRow
        key={entry.type+'_'+(parseInt(entry.nodeinfo.id)*2).toString()+'_'+i}
        id={entry.type+'_'+(parseInt(entry.nodeinfo.node_object_id)*2).toString()+'_'+i}
        name={entry.name}
        directory={entry.directory}
        instructor={entry.instructor}
        '''institution={entry.institution}'''
        creation_date={entry.creation_date}
        updated_date={entry.updated_date}
        actions={entry.actions}
        is_available={entry.is_available}
        course_id={entry.course_id}
        max_team_size={entry.max_team_size}
        is_intelligent={entry.is_intelligent}
        require_quiz={entry.require_quiz}
        dataType={_this.props.dataType}
        //this is just a hack. All current users courses are marked as private during fetch for display purpose.
        private={entry.private}
        allow_suggestions={entry.allow_suggestions}
        has_topic={entry.has_topic}
        rowClicked={_this.handleExpandClick}
        newParams={entry.newParams}
      />)


<br>
Sorting in Ascending Order:


=== Issue #926: ===
[[file:ascending.png]]
This task called for removing the 'Actions' column on the signup sheet for assignments that have been completed (sign_up_sheet/list.html.erb).


Sorting in Descending Order :


(sign_up_sheet/list.html.erb) renders '''app/view/sign_up_sheet/_table_line.html.erb''' and an if statement is used to not show finished assignements.
[[file:descending.png]]


  <% if @assignment.current_stage_name(@topic_id) != 'Finished' %>
=== Issue #718: ===
    <td align="center"><%= render :partial => '/sign_up_sheet/actions', :locals => {:i=>i,:topic=>topic, :is_suggested_topic=>is_suggested_topic ||= false} %></td>
  <% end %>


<br>
Instructors should be allowed to give feedback when accepting or rejecting topic suggestions. Earlier, feedback on topics suggested by students can be given only when the instructor wants the topic to be revised, not when (s)he is approving or rejecting it. Feedback should be possible in any of these cases.


In ''''' app/controllers/suggestion_controller.rb'''''


=== Issue #718: ===
Including the text provided in the comment field, while approving the topic. This ensures that comments are saved when an instructor accepts a topic suggestion in the backend.
This task called for adding a one-time notification at the top of the page to show changes/notifications. It necessitated adding a table to the database called notificationsTherefore, when conducting peer reviews, a migration must be performed to add the new table. Then we used scaffolding to add notifications (controller, model, and views). Next we added a link on the view (app/view/tree_display/index.html.erb) to go to the view (app/view/notification/index.html.erb) in order to manage notifications.  
  def approve
  # 1781 - 718 issue
  # The Instructor should be able to give feed backs during the times of approval as well.
  # Thus we are getting the comments through the request when approval is made and saving those in
  # Database with Vote type as A - meaning approval
  if params[:suggestion_comment][:comments] && params[:suggestion_comment][:comments] != ""
    @suggestioncomment = SuggestionComment.new(vote: 'A', comments: params[:suggestion_comment][:comments])
    @suggestioncomment.suggestion_id = params[:id]
    @suggestioncomment.commenter = session[:user].name
    @suggestioncomment.save


Including the text provided in the comment field, while rejecting the topic.This ensures that comments are saved when an instructor rejects a topic suggestion in the backend.


A migration to create a '''notifications''' table was added.
  def reject_suggestion
 
  # 1781 - 718 issue
<pre>
  # The Instructor should be able to give feed backs during the times of rejection as well.
   class CreateNotifications < ActiveRecord::Migration
  # Thus we are getting the comments through the request when denial is made and saving those in
     def change
  # Database with Vote type as D - meaning reject
      create_table :notifications do |t|
   if params[:suggestion_comment][:comments] && params[:suggestion_comment][:comments] != ""
        t.string :subject
     @suggestioncomment = SuggestionComment.new(vote: 'D', comments: params[:suggestion_comment][:comments])
        t.text :description
    @suggestioncomment.suggestion_id = params[:id]
        t.date :expiration_date
    @suggestioncomment.commenter = session[:user].name
        t.boolean :active_flag
     @suggestioncomment.save
        t.timestamps null: false
      end
     end
   end
   end
</pre>


In ''''' app/views/suggestion/show.html.erb'''''


Using scaffolding, a new index was added '''app/view/notifications/index.html.erb'''.
The following piece of code is to handle scenarios in frontend to list the comments even for approval and deniel of suggestions.


<pre>
<pre>
  <h1>Listing Notifications</h1>
  <table>
    <thead>
      <tr>
        <th>Subject</th>
        <th>Description</th>
        <th>Expiration date</th>
        <th>Active flag</th>
        <th colspan="3"></th>
      </tr>
    </thead>
    <tbody>
      <% @notifications.each do |notification| %>
        <tr>
          <td><%= notification.subject %></td>
          <td><%= notification.description %></td>
          <td><%= notification.expiration_date %></td>
          <td><%= notification.active_flag %></td>
          <td><%= link_to 'Show', notification %></td>
          <td><%= link_to 'Edit', edit_notification_path(notification) %></td>
          <td><%= link_to 'Destroy', notification, method: :delete, :confirm => 'Are you sure?' %></td>
        </tr>
      <% end %>
    </tbody>
  </table>


  <br>
    <% for suggestion_comment in @suggestion.suggestion_comments %>
      <tr class="listingRow">
        <td align="center"><%=suggestion_comment.commenter %></td>
        <td align="center">
          <%if suggestion_comment.vote == 'Y' %>
            Yes
          <%elsif suggestion_comment.vote == 'N'%>
            No
          <%elsif suggestion_comment.vote == 'R'%>
            Revise
            <!-- Two more crieteria for displaying comments. During Approval and Denial. Issue #1781 -->
          <%elsif suggestion_comment.vote == 'A'%>
            Approved
          <%elsif suggestion_comment.vote == 'D'%>
            Denied
          <%else%>
            --
          <%end%>


  <%= link_to 'New Notification', new_notification_path %>
</pre>
</pre>


Only an instructor should be able to approve or reject a submission. Therefore, only for instructor role, approve and reject submission options will be provided.


Added link on page for managers to manage notifications '''app/view/tree_display/list.html.erb'''.
[[File:256.png]]
Notifications are added and modified in similar views '''app/view/tree_display/new.html.erb''' or '''app/view/tree_display/edit.html.erb'''.
[[File:Notifications.png]]
When any type of user successfully logs in, flash notifications called from the controller '''app/controllers/auth_controller.rb'''.
<pre>
<pre>
  def after_login(user)
      <% if @suggestion.status != 'Approved' && @suggestion.status != 'Rejected' && session[:user] != nil && (session[:user].role_id == 2 || session[:user].role_id == 3 || session[:user].role_id == 4) %>
    session[:user] = user
        <%= submit_tag "Approve suggestion", :name => 'approve_suggestion' %>
    AuthController.set_current_role(user.role_id, session)
        <%= submit_tag "Reject suggestion", :name => 'reject_suggestion' %>
 
      <% end %>
    flash[:notification] = 'This will display notifications on login'
 
    redirect_to controller: AuthHelper.get_home_controller(session[:user]),
                action: AuthHelper.get_home_action(session[:user])
  end
</pre>
</pre>


Comments can be added even during topic approval or rejection.


Flash notifications are displayed once at login due to a line in the view '''app/view/layouts/application.html.erb''' which is used by all other views.
[[File:Approve_suggestion.png]]
 
<%= render 'shared/flash_notifications' %>
 
 
Each individual flash notification that is active and is not expired is displayed through the use of the view '''app/view/shared/_flash_notifications.html.erb'''.
 
<pre>
  <% if  (flash_message :notification) != nil %>
    <% @notifications = Notification.where(:active_flag => true) %>
    <% if !@notifications.nil? %>
      <% @notifications.each do |notification| %>
        <% if notification.expiration_date >= Date.today %>
          <table style="background-color: #faebcc">
            <tbody style="background-color: #faebcc"><br>
              <tr><p style="color:black; font-width:bold; background-color: #faebcc"><%= notification.subject.upcase %></p></tr>
              <tr><%= notification.description %></tr>
            </tbody>
          </table>
        <% end %>
      <% end %>
    <% end %>
  <% end %>
</pre>




The notifications are displayed on whatever page the login screen goes to first depending of the type of user logging into the system.  They are only display once and are gone whenever the page is refreshed or changed.
Comments during approval or rejection get reflected in the feedback list.


[[File:Notifications_view.png]]
[[File:After_approval_suggestion.png]]


<br>
<br>


== Testing Plan:  Testing from the UI ==
== Testing Plan ==
=== UI Testing ===
=== UI Testing ===
The majority of the changes can be tested via the UI. Follow the instructions below to check the tasks:
Screencast of the demo: https://youtu.be/fCxhD-qFUro
The majority of the changes can be tested via the UI. Follow the instructions below to check the tasks.


* '''Issue #971:''' Change create topic UI into AJAX
* '''Issue #971:''' Change create topic UI into AJAX
Line 299: Line 351:
# Log out once the notification is created.
# Log out once the notification is created.
# Log in using any account.  The notification will display on the first page the user is shown.  It will disappear when they change or reload the page.  Note: instructors on the management page will retain the notification if they switch between Assignments, Courses, and Questionnaires.  This is because the JavaScript does not actually reload the page.
# Log in using any account.  The notification will display on the first page the user is shown.  It will disappear when they change or reload the page.  Note: instructors on the management page will retain the notification if they switch between Assignments, Courses, and Questionnaires.  This is because the JavaScript does not actually reload the page.
=== Automated Test ===
Rspec tests for the new load_add_signup_topics_method implemented in the signup sheet controller, is implemented in spec/controllers/sign_up_sheet_controller_spec.rb file. In the load_add_signup_topics method, the existing data is converted to json and rendered in the form of a jsgrid. So the test method creates a mock assignment object and invokes the load_add_signup_topics method on that assignment. If the assignment is found, it loads all the topics and checks if the rendered format is json along with a http status 'OK'. If the assignment is not found, an empty json is rendered. So a http status of 'not found' is verified.
In ''''' spec/controllers/sign_up_sheet_controller_spec.rb '''''
  describe '#load_add_signup_topics' do
    context 'when assignment is found' do
      it 'should render json successfully' do
        allow(SignUpTopic).to receive(:where).and_return([topic])
        get :load_add_signup_topics, id: "#{assignment.id}"
        expect(response).to have_http_status(:ok)
        expect(response.content_type).to eq "application/json"
        expect(response.body).to include_json({
          id: assignment.id.to_s,
          sign_up_topics: [{
            id: topic.id,
            topic_name: "#{topic.topic_name}",
            assignment_id: assignment.id,
            max_choosers: topic.max_choosers,
            topic_identifier: "#{topic.topic_identifier}",
            micropayment: topic.micropayment,
            slots_filled_value: 0,
            slots_waitlisted: 0,
            slots_available: 1,
            partipants: []
          }],
          slots_waitlisted: [],
          assignment: {
            id: assignment.id
          }
        }
        )
      end
    end
    context 'when assignment is not found' do
      it 'should render empty json successfully' do
        allow(Assignment).to receive(:find).and_raise(ActiveRecord::RecordNotFound)
        get :load_add_signup_topics, id: "#{assignment.id}"
        puts response.body
        expect(response.content_type).to eq "application/json"
        expect(response).to have_http_status(:not_found)
      end
    end
  end

Latest revision as of 01:51, 27 November 2017

This wiki page describes the changes made according to the specification of E1781 OSS assignment for Fall 2017.

Peer Review Information

For testing the changes made, the following credentials are recommended:

  • Instructor Login: username: instructor6 password: password

This project does compile. Reviewers can refer Testing Plan: Testing from the UI and the YouTube link https://youtu.be/fCxhD-qFUro for navigation help.

Introduction

Background

Expertiza is a web portal which can be used to manage assignments related to a course. It provides a platform to view assignments, manage teams, select topics and work on improvement through anonymous peer reviews.

Problem Statement

Expertiza allows the instructor to define different topics that students or teams could choose from as their assignment. Each topic can have 0 or more slots that indicate the number of students or teams that can be signed up for that topic. To improve the user experience when managing topics, the following tasks were identified:

  • Issue #971: Change create topic UI into AJAX
  • Issue #926: We need a way to sort topics by topic number in assignment#edit page
  • Issue #718: We should allow instructors to give feedback when accepting or rejecting topic suggestions

Files Modified

The following files were modified for this project:

  • app/assets/javascripts/application.js
  • app/assets/javascripts/signup.js
  • app/assets/stylesheets/application.scss
  • app/controllers/sign_up_sheet_controller.rb
  • app/models/sign_up_topic.rb
  • app/views/assignments/edit.html.erb
  • app/views/layouts/application.html.erb
  • app/views/sign_up_sheet/_add_signup_topics.html.erb
  • app/views/sign_up_sheet/_add_signup_topics_staggered.html.erb
  • bower.json
  • config/routes.rb
  • Gemfile - changed json from version 1.8.3 to 1.8.5

Files Added

The following files were added to this project:

  • app/assets/images/icons-2x.png
  • app/assets/images/icons.png

Solutions implemented and Delivered

Issue #971: Change create topic UI into AJAX

Currently, when instructors manually enter topics, they have to go back and forth between the list of the topic page (views>sign_up_sheet>_add_signup_topics.html.erb) and the create topic page(views>sign_up_sheet>new.html.erb). Adding a new topic can be done through an editable grid (see js-grid) without leaving the list of topic page, by making use of AJAX. The list should be automatically updated when a new topic is entered. To enable AJAX the sign_up_sheet_controller.rb is modified so that it renders JSON to handle the javascript table/form.

In addition, when adding a topic, the default slot is set as 1 instead of 0. The current warning message that shows up when the slot is 0, has been fixed to close properly.

On the controller app/controllers/sign_up_sheet_controller.rb we edited the Load_add_signup_topics, which was converted from a function into a Rest endpoint(Json).

 # retrieves all the data associated with the given assignment. Includes all topics,
 # this should retrieve results in Json so that it can be ajaxed
 # 1781
 # the following method is an action which renders all the topics of an assignment in the JSON format.
 def load_add_signup_topics
   @id = params[:id]
   assignment_id =  params[:id]
   @sign_up_topics = SignUpTopic.where('assignment_id = ?', assignment_id)
   @slots_filled = SignUpTopic.find_slots_filled(assignment_id)
   @slots_waitlisted = SignUpTopic.find_slots_waitlisted(assignment_id)
   @assignment = Assignment.find(assignment_id)
   @participants = SignedUpTeam.find_team_participants(assignment_id)
   # colloborating for JSON
   @sign_up_topics.each {|topic|
     topic_id = topic.id
     slots_fill_temp = 0
     slots_waitlisted = 0
     participants = []
     if @slots_filled
       @slots_filled.each {|slot|
         if slot.topic_id == topic_id
           slots_fill_temp = slot.count
         end
       }
     end
     if @slots_waitlisted
       @slots_waitlisted.each {|slot|
         if slot.topic_id == topic_id
           slots_waitlisted = slot.count
         end
       }
     end
     if @participants
       @participants.each {|participant|
         if participant.topic_id == topic_id
           participants << participant
         end
       }
     end
     topic.slots_filled_value = slots_fill_temp
     topic.slots_waitlisted = slots_waitlisted
     topic.slots_available = topic.max_choosers - topic.slots_filled_value
     topic.partipants = participants
   }
   # ACS Removed the if condition (and corresponding else) which differentiate assignments as team and individual assignments
   # to treat all assignments as team assignments
   # Though called participants, @participants are actually records in signed_up_teams table, which
   # is a mapping table between teams and topics (waitlisted recored are also counted)
   render :json => {
     :id => @id.as_json,
     :sign_up_topics => @sign_up_topics.as_json( :methods => [:slots_filled_value,:slots_waitlisted,:slots_available,:partipants]),
     :slots_waitlisted => @slots_waitlisted.as_json,
     :assignment => @assignment.as_json
   }
 end

An Example output of the Json rendered for Assignment Id : 843 by the load_add_signup_topics Action is as follows:

{ "id": "843", "sign_up_topics": [ { "id": 3958, "topic_name": "Power consumption issue", "assignment_id": 843, "max_choosers": 3, "category": "", "topic_identifier": "1.1.1", "micropayment": 0, "private_to": null, "description": null, "link": null, "slots_filled_value": 0, "slots_waitlisted": 0, "slots_available": 3, "partipants": [] }, { "id": 3959, "topic_name": "Perspectives on parallel computers (1.4 in old edition)", "assignment_id": 843, "max_choosers": 2, "category": "", ... (And so on.)

Similarly, all the actions/methods (destroy|setup_new_topic|update_existing_topic|update|set_values_for_new_topic ) were changed to render Json rather than rendering a view, in order to satisty the Ajax requirements in the front end.

On the model app/models/sign_up_topic.rb an attribute accessor was created for variables to set properties that are required for the JSON.

 attr_accessor :slots_filled_value
 attr_accessor :slots_waitlisted
 attr_accessor :slots_available
 attr_accessor :partipants

On the View 'app/views/sign_up_sheet/_add_signup_topics.html.erb' the following was added:

<%= content_tag :div, id: "jsGrid", data: { assignmentid: @assignment.id }  do %>
<% end %>

The content of the above tag is completely rendered via Javascript, where we use JSGrid for dynamically allowing users to Add, Delete and Update Topics on the same page.

In ' app/assets/javascripts/signup.js ' file, the following was added: The following content shows how AJAX calls are handled/made to our signup controller so that the CRUD operations on the topics page is dealt with

 /* controller Object : All the CRUD functionalities of our JS GRID is taken care under this controller object*/
        controller: {
            /*This makes an Ajax call to load all the signup topics which will be rendered as a json from endpoint */
            loadData: function(filter) {
                var data = $.Deferred();
                $.ajax({
                    type: "GET",
                    contentType: "application/json; charset=utf-8",
                    url: "/sign_up_sheet/" + getAssignmentId() + "/load_add_signup_topics", // end point for making the load topics 
                    dataType: "json"
                }).done(function(response) {
                    var sign_up_topics = response.sign_up_topics;
                    data.resolve(sign_up_topics);
                }).fail(function(response) {
                    alert("Issue on Loading Topics"); // If problem occurs in loading topics
                    data.resolve(response);
                });
                return data.promise();
            },

            /*This makes an Ajax call to insert a new  signup topic which will be entered by the user as topic argument */
            insertItem: function(topic) {
                topic.id = getAssignmentId() // the data to be sent through Ajax should be having the assignment id in id field
                var data = $.Deferred();
                $.ajax({
                    type: "POST",
                    url: "/sign_up_sheet/",
                    data: topic
                }).done(function(response) {
                    jQuery("#jsGrid").jsGrid("loadData");
                    data.resolve(response);
                }).fail(function(response) {
                    var responseJson = response;
                    data.resolve(response);
                    // this is the special case when user tries to set slots than slots already booked
                    if (responseJson.status == 400) {
                        jQuery(document).scrollTop(0);
                        location.reload();
                    } else
                        alert(responseJson.responseText);
                });
                return data.promise();
            },

            /*This makes an Ajax call to update a single record in our JS Grid when user makes changes to it*/
            updateItem: function(topic) {
                var data = $.Deferred();
                $.ajax({
                    type: "PUT",
                    url: "/sign_up_sheet/" + topic.id,
                    data: topic
                }).done(function(response) {
                    jQuery("#jsGrid").jsGrid("loadData");
                    data.resolve(response);
                }).fail(function(response) {
                    var responseJson = response;
                    data.resolve(previousItem);
                    // this is the special case when user tries to set slots than slots already booked
                    if (responseJson.status == 400) {
                        jQuery(document).scrollTop(0);
                        location.reload();
                    } else
                        alert(responseJson.responseText);
                });
                return data.promise();
            },

            /*This makes an Ajax call to delete a single record*/
            deleteItem: function(topic) {
                return $.ajax({
                    type: "DELETE",
                    url: "/sign_up_sheet/" + topic.id
                });
            }
        }

The Preview View for Topics Management :

After Our Implementation using JS Grids :

Issue #926:

This task is to do with sorting the Topics according to the topic number. This functionality is taken care by Js-Grid by itself, where clicking the topic# will toggle the topics in the ascending/descending order.

Sorting in Ascending Order:

Sorting in Descending Order :

Issue #718:

Instructors should be allowed to give feedback when accepting or rejecting topic suggestions. Earlier, feedback on topics suggested by students can be given only when the instructor wants the topic to be revised, not when (s)he is approving or rejecting it. Feedback should be possible in any of these cases.

In app/controllers/suggestion_controller.rb

Including the text provided in the comment field, while approving the topic. This ensures that comments are saved when an instructor accepts a topic suggestion in the backend.

def approve
 # 1781 - 718 issue
 # The Instructor should be able to give feed backs during the times of approval as well.
 # Thus we are getting the comments through the request when approval is made and saving those in 
 # Database with Vote type as A - meaning approval 
 if params[:suggestion_comment][:comments] && params[:suggestion_comment][:comments] != ""
   @suggestioncomment = SuggestionComment.new(vote: 'A', comments: params[:suggestion_comment][:comments])
   @suggestioncomment.suggestion_id = params[:id]
   @suggestioncomment.commenter = session[:user].name
   @suggestioncomment.save

Including the text provided in the comment field, while rejecting the topic.This ensures that comments are saved when an instructor rejects a topic suggestion in the backend.

 def reject_suggestion
 # 1781 - 718 issue
 # The Instructor should be able to give feed backs during the times of rejection as well.
 # Thus we are getting the comments through the request when denial is made and saving those in 
 # Database with Vote type as D - meaning reject 
 if params[:suggestion_comment][:comments] && params[:suggestion_comment][:comments] != ""
   @suggestioncomment = SuggestionComment.new(vote: 'D', comments: params[:suggestion_comment][:comments])
   @suggestioncomment.suggestion_id = params[:id]
   @suggestioncomment.commenter = session[:user].name
   @suggestioncomment.save
 end

In app/views/suggestion/show.html.erb

The following piece of code is to handle scenarios in frontend to list the comments even for approval and deniel of suggestions.


    <% for suggestion_comment in @suggestion.suggestion_comments %>
      <tr class="listingRow">
        <td align="center"><%=suggestion_comment.commenter %></td>
        <td align="center">
          <%if suggestion_comment.vote == 'Y' %>
            Yes
          <%elsif suggestion_comment.vote == 'N'%>
            No
          <%elsif suggestion_comment.vote == 'R'%>
            Revise
            <!-- Two more crieteria for displaying comments. During Approval and Denial. Issue #1781 -->
          <%elsif suggestion_comment.vote == 'A'%> 
            Approved
          <%elsif suggestion_comment.vote == 'D'%>
            Denied
          <%else%>
            --
          <%end%>

Only an instructor should be able to approve or reject a submission. Therefore, only for instructor role, approve and reject submission options will be provided.

       <% if @suggestion.status != 'Approved' && @suggestion.status != 'Rejected' && session[:user] != nil && (session[:user].role_id == 2 || session[:user].role_id == 3 || session[:user].role_id == 4) %>
         <%= submit_tag "Approve suggestion", :name => 'approve_suggestion' %>
         <%= submit_tag "Reject suggestion", :name => 'reject_suggestion' %>
       <% end %>

Comments can be added even during topic approval or rejection.


Comments during approval or rejection get reflected in the feedback list.


Testing Plan

UI Testing

Screencast of the demo: https://youtu.be/fCxhD-qFUro The majority of the changes can be tested via the UI. Follow the instructions below to check the tasks.

  • Issue #971: Change create topic UI into AJAX
  1. Login as an instructor. It will automatically go to the manage courses page
  2. Click Assignments tab next to Courses
  3. Select the assignment for which a new topic has to be added
  4. Click the edit icon for the assignment and go to Topics tab
  5. You will be able to see the topics list in a grid format. Click on add icon on the right side corner of the grid
  6. You can also edit an existing entry by just clicking on that row in the grid without being redirected to a new page
  • Issue #926: Sort topics by topic number in assignment#edit page.
  1. Log in as any user or instructor.
  2. Go to the assignments list. Note: this is not the manage assignments for instructors.
  3. Click into a finished assignment. You will notice the actions column is gone.
  4. Click into a current assignment. You will notice the actions column appears.
  • Issue #718:
  1. Login as an instructor. It will automatically go to the manage courses page.
  2. Click on the "Manage Notifications" link above the "Courses" and "Assignments" links. Note: The link was placed here because the system has numerous issues when adding menu items. It would require us to do more work that the entirety of this project to correct the menu additions in the superuser menu.
  3. You will be directed to a creation page for notifications. Note: in order for a notification to display the expiration date must be the current date or later, and the active checkbox must be selected.
  4. Log out once the notification is created.
  5. Log in using any account. The notification will display on the first page the user is shown. It will disappear when they change or reload the page. Note: instructors on the management page will retain the notification if they switch between Assignments, Courses, and Questionnaires. This is because the JavaScript does not actually reload the page.

Automated Test

Rspec tests for the new load_add_signup_topics_method implemented in the signup sheet controller, is implemented in spec/controllers/sign_up_sheet_controller_spec.rb file. In the load_add_signup_topics method, the existing data is converted to json and rendered in the form of a jsgrid. So the test method creates a mock assignment object and invokes the load_add_signup_topics method on that assignment. If the assignment is found, it loads all the topics and checks if the rendered format is json along with a http status 'OK'. If the assignment is not found, an empty json is rendered. So a http status of 'not found' is verified.

In spec/controllers/sign_up_sheet_controller_spec.rb

 describe '#load_add_signup_topics' do
    context 'when assignment is found' do
      it 'should render json successfully' do
        allow(SignUpTopic).to receive(:where).and_return([topic])
        get :load_add_signup_topics, id: "#{assignment.id}"
        expect(response).to have_http_status(:ok)
        expect(response.content_type).to eq "application/json"
        expect(response.body).to include_json({
          id: assignment.id.to_s,
          sign_up_topics: [{
            id: topic.id,
            topic_name: "#{topic.topic_name}",
            assignment_id: assignment.id,
            max_choosers: topic.max_choosers,
            topic_identifier: "#{topic.topic_identifier}",
            micropayment: topic.micropayment,
            slots_filled_value: 0,
            slots_waitlisted: 0,
            slots_available: 1,
            partipants: []
          }],
          slots_waitlisted: [],
          assignment: {
            id: assignment.id
          }
        }
        )
      end
    end
    context 'when assignment is not found' do
      it 'should render empty json successfully' do
        allow(Assignment).to receive(:find).and_raise(ActiveRecord::RecordNotFound)
        get :load_add_signup_topics, id: "#{assignment.id}"
        puts response.body
        expect(response.content_type).to eq "application/json"
        expect(response).to have_http_status(:not_found)
      end
    end
  end