CSC/ECE 517 Fall 2017/E1781 Topic Management
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.
The testing plan is at the end of this document. Design principles were not needed since we mostly modified existing work.
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
The following were the tasks identified to be accomplished through this project.
- 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 in this Project
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 in this Project
The following files were added for 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).
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 } 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 follwing 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.
jQuery("#jsGrid").jsGrid({ width: "100%", height : "400%", filtering: true, inserting: true, editing: true, sorting: true, paging: true, autoload: true, updateOnResize : true, deleteConfirm: "Do you really want to delete client?", controller: { loadData: function (filter) { var data = $.Deferred(); $.ajax({ type: "GET", contentType: "application/json; charset=utf-8", url: "/sign_up_sheet/"+jQuery("#jsGrid").data("assignmentid")+"/load_add_signup_topics", // url: "/sign_up_sheet/847/load_add_signup_topics", dataType: "json" }).done(function(response){ var sign_up_topics = response.sign_up_topics; data.resolve(sign_up_topics); }); return data.promise(); }, insertItem: function (topic) { console.log("testing") console.log(topic) topic.id = jQuery("#jsGrid").data("assignmentid") var data = $.Deferred(); $.ajax({ type: "POST", url: "/sign_up_sheet/", // url: "/sign_up_sheet/847/load_add_signup_topics", data: topic }).done(function(response){ data.resolve(response); }); return data.promise(); }, updateItem: function (topic) { console.log("testing") console.log(topic) var data = $.Deferred(); $.ajax({ type: "PUT", url: "/sign_up_sheet/"+topic.id, // url: "/sign_up_sheet/847/load_add_signup_topics", data: topic }).done(function(response){ data.resolve(response); }); return data.promise(); }, deleteItem: function(item) { return $.ajax({ type: "DELETE", url: "/sign_up_sheet/" + item.id }); } }, fields: [ { name: "topic_identifier", type: "text" ,title: "Topic #",width : "1.5%" }, { name: "topic_name", type: "text" ,title: "Topic name(s)",width : "5%", itemTemplate: function(value,topic) { var linkText = $("<a>").attr("href", topic.link).text(value); var signupUrl = "/sign_up_sheet/signup_as_instructor?assignment_id=" + assignmentId + "&topic_id="+topic.id; var signUpUser = $("<a>").attr("href", signupUrl); var signUpUserImage = $("<img>").attr({src: "/assets/signup- 806fc3d5ffb6923d8f5061db243bf1afd15ec4029f1bac250793e6ceb2ab22bf.png" , title: "Sign Up Student" , alt: "Signup"}); //participants var participants_temp = topic.partipants; if(participants_temp == null) participants_temp = []; var participants_div = $('< div >'); for(var p = 0 ; p < participants_temp.length ; p ++) { var current_participant = participants_temp[p]; var text = $("<span>"); text.html(current_participant.user_name_placeholder); var dropStudentUrl = "/sign_up_sheet/delete_signup_as_instructor/" + current_participant.team_id + "? topic_id="+topic.id; var dropStudentAnchor = $("<a>").attr("href", dropStudentUrl); var dropStudentImage = $("<img>").attr({src: "/assets/delete_icon.png" , title: "Drop Student" , alt: "Drop Student Image"}); participants_div.append(text).append(dropStudentAnchor.append(dropStudentImage)); } return $('< div >').append(linkText).append(signUpUser.append(signUpUserImage)).append(participants_div); } , filtering: true }, { name: "category", type: "text",title: "Topic category" ,width : "5%" }, { name: "max_choosers", type: "text" ,title: "# Slots" ,width : "2%"}, { name: "slots_available", editing: false ,title: "Available Slots",width : "2%"}, { name: "slots_waitlisted", editing: false ,title: "Num on Waitlist" , width : "2%"}, { name: "id",title: "Book marks",width :"20%", editing: false,width :"2%", itemTemplate: function(value, topic) { console.log("value ",value) console.log("topic ",topic) var $customBookmarkAddButton = $("<a>").attr({ href:"/bookmarks/list/"+topic.id }); var $BookmarkSelectButton= $("<i>").attr({class :"jsgrid-bookmark-show fa fa-bookmark", title :"View Topic Bookmarks"}); var $customBookmarkSetButton = $("<a>").attr({ href:"/bookmarks/new?id="+topic.id }); var $BookmarkSetButton= $("<i>").attr({class :"jsgrid-bookmark-add fa fa-plus" , title :"Add Bookmark to Topic"}); var set1 = $customBookmarkAddButton.append($BookmarkSelectButton); var set2 = $customBookmarkSetButton.append($BookmarkSetButton); return $('< div >').attr("align","center").append(set1).append(set2); } /* itemTemplate: function(value, topic) { console.log("value ",value) console.log("topic ",topic) return $customBookmarkSetButton.append($BookmarkSetButton); }*/ ,filtering: true }, { type: "control", editButton: true, // show edit button deleteButton: true, // show delete button clearFilterButton: true, // show clear filter button modeSwitchButton: true, // show switching filtering/inserting button width : "3%" } , { name: "link", type: "text",title: "Topic Link" ,width :"12%" }, { name: "description", type: "textarea",title: "Topic Description",width :"12%" } ] }); });
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.
def approve 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 end
Including the text provided in the comment field, while rejecting the topic.
def reject_suggestion 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
<% 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 <%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 gets 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
- Login as an instructor. It will automatically go to the manage courses page
- Click Assignments tab next to Courses
- Select the assignment for which a new topic has to be added
- Click the edit icon for the assignment and go to Topics tab
- 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
- 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.
- Log in as any user or instructor.
- Go to the assignments list. Note: this is not the manage assignments for instructors.
- Click into a finished assignment. You will notice the actions column is gone.
- Click into a current assignment. You will notice the actions column appears.
- Issue #718:
- Login as an instructor. It will automatically go to the manage courses page.
- 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.
- 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.
- 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.
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