Redesign the To-Do, Coming Up, and Recent Feedback lists

Fixes CNVS-25877

Test plan:
 - Create a user with:
   - an assignment that needs to be submitted
   - an assignment that needs to be graded
   - an assignment whose grades the user needs to moderate
   - an assignment that they need to peer review
   - a calendar event that they can see
 - Go to the user's home page
 - Ensure all of the assignments show up in the to-do list
 - Ensure they show up in Coming Up as well
 - Ensure that the calendar event shows up under Coming Up
 - Go to the courses where you created the assignments
 - Ensure that each assignment shows up on its respective course to-do
   list
 - Submit an assignment
 - As a teacher, ensure that the assignment shows up as needing
   grading, and that the number in the badge to the left is 1
 - Ensure that screenreaders read the badge as "1 submission
   needs grading"
 - Make another submission as a different user
 - Clear the cache by running `Rails.cache.clear` at a Rails console
 - As the teacher, ensure that the badge is now 2
 - Ensure that screenreaders read the badge as "2 submissions
   need grading"
 - Repeat 8 more times as different users, so that there are now
   10 submissions
 - Clear the cache again
 - Ensure that the badge now says "9+"
 - Ensure that screenreaders read the badge as "More than 9
   submissions need grading"
 - As the user who submitted the assignment, ensure that the grade
   shows up on the To-Do list, Coming Up, and Recent Feedback
 - As a teacher, leave a submission comment
 - As the user who submitted the assignment, ensure that the comment
   shows up under Recent Feedback
 - Create a public course
   - You can make a course public by going to its settings page
     and checking "Make this course publicly visible"
 - Log out
 - Visit /courses/<id>, where <id> is the id of the course
 - Ensure that the course shows as expected, and that nothing
   shows up on the to-do list
 - Automated tests should cover everything else

Change-Id: I18673995db94d896bf2c39515258e61065b48319
Reviewed-on: https://gerrit.instructure.com/69474
Reviewed-by: Andrew Butterfield <abutterfield@instructure.com>
QA-Review: Heath Hales <hhales@instructure.com>
Tested-by: Jenkins
Product-Review: Allison Weiss <allison@instructure.com>
This commit is contained in:
Alex Boyd 2015-12-28 17:39:37 -07:00
parent db87c8a555
commit 7799495aa3
19 changed files with 669 additions and 361 deletions

View File

@ -534,12 +534,12 @@ class UsersController < ApplicationController
end
def cached_submissions(user, upcoming_events)
Rails.cache.fetch(['cached_user_submissions', user].cache_key,
Rails.cache.fetch(['cached_user_submissions2', user].cache_key,
:expires_in => 3.minutes) do
assignments = upcoming_events.select{ |e| e.is_a?(Assignment) }
Shard.partition_by_shard(assignments) do |shard_assignments|
Submission.
select([:id, :assignment_id, :score, :workflow_state, :updated_at]).
select([:id, :assignment_id, :score, :grade, :workflow_state, :updated_at]).
where(:assignment_id => shard_assignments, :user_id => user)
end
end

View File

@ -43,12 +43,16 @@ module AssignmentsHelper
link_to submission_author_name_for(assessment), context_url(context, :context_assignment_submission_url, assignment.id, assessment.asset.user_id), link_options
end
def due_at(assignment, user, format='datetime')
def due_at(assignment, user)
if assignment.multiple_due_dates_apply_to?(user)
multiple_due_dates(assignment)
else
assignment = assignment.overridden_for(user)
send("#{format}_string", assignment.due_at, :short)
if assignment.due_at
datetime_string(assignment.due_at)
else
I18n.t('No Due Date')
end
end
end

View File

@ -0,0 +1,201 @@
class ToDoListPresenter
ASSIGNMENT_LIMIT = 100
VISIBLE_LIMIT = 5
attr_reader :needs_grading, :needs_moderation, :needs_submitting, :needs_reviewing
def initialize(view, user, contexts)
@view = view
@user = user
@contexts = contexts
if user
@needs_grading = assignments_needing(:grading)
@needs_moderation = assignments_needing(:moderation)
@needs_submitting = assignments_needing(:submitting)
assessment_requests = user.submissions_needing_peer_review(contexts: contexts, limit: ASSIGNMENT_LIMIT)
@needs_reviewing = assessment_requests.map do |ar|
AssessmentRequestPresenter.new(view, ar, user) if ar.asset.assignment.published?
end.compact
else
@needs_grading = []
@needs_moderation = []
@needs_submitting = []
@needs_reviewing = []
end
end
def assignments_needing(type)
if @user
@user.send("assignments_needing_#{type}", contexts: @contexts, limit: ASSIGNMENT_LIMIT).map do |assignment|
AssignmentPresenter.new(@view, assignment, @user, type)
end
else
[]
end
end
def any_assignments?
@user && (
@needs_grading.present? ||
@needs_moderation.present? ||
@needs_submitting.present? ||
@needs_reviewing.present?
)
end
# False when there's only one context (no point in showing its name beneath each assignment), true otherwise.
def show_context?
@contexts.nil? || @contexts.length > 1
end
def visible_limit
VISIBLE_LIMIT
end
def hidden_count_for(items)
if items.length > visible_limit
items.length - visible_limit
else
0
end
end
def hidden_count
@hidden_count ||= [needs_grading, needs_moderation, needs_submitting, needs_reviewing].sum do |items|
hidden_count_for(items)
end
end
class AssignmentPresenter
attr_reader :assignment
protected :assignment
delegate :title, :submission_action_string, :points_possible, :due_at, :peer_reviews_due_at, to: :assignment
def initialize(view, assignment, user, type)
@view = view
@assignment = assignment
@user = user
@type = type
end
def needs_moderation_icon_data
@view.icon_data(context: assignment.context, current_user: @user, recent_event: assignment)
end
def needs_submitting_icon_data
@view.icon_data(context: assignment.context, current_user: @user, recent_event: assignment, student_only: true)
end
def context_name
@assignment.context.nickname_for(@user)
end
def needs_grading_count
@needs_grading_count ||= Assignments::NeedsGradingCountQuery.new(@assignment, @user).count
end
def needs_grading_badge
if needs_grading_count > 9
I18n.t('%{more_than}+', more_than: 9)
else
needs_grading_count
end
end
def needs_grading_label
if needs_grading_count > 9
I18n.t('More than 9 submissions need grading')
else
I18n.t({one: '1 submission needs grading', other: '%{count} submissions need grading'}, count: assignment.needs_grading_count)
end
end
def gradebook_path
@view.speed_grader_course_gradebook_path(assignment.context_id, assignment_id: assignment.id)
end
def moderate_path
@view.course_assignment_moderate_path(assignment.context_id, assignment)
end
def assignment_path
@view.course_assignment_path(assignment.context_id, assignment.id)
end
def ignore_url
@view.todo_ignore_api_url(@type, @assignment)
end
def ignore_title
case @type
when :grading
I18n.t('Ignore until new submission')
when :moderation
I18n.t('Ignore until new mark')
when :submitting
I18n.t('Ignore this assignment')
end
end
def ignore_flash_message
case @type
when :grading
I18n.t('This item will reappear when a new submission is made.')
when :moderation
I18n.t('This item will reappear when there are new grades to moderate.')
end
end
def formatted_due_date
@view.due_at(assignment, @user)
end
def formatted_peer_review_due_date
if assignment.peer_reviews_due_at
@view.datetime_string(assignment.peer_reviews_due_at)
else
I18n.t('No Due Date')
end
end
end
class AssessmentRequestPresenter
delegate :context_name, to: :assignment_presenter
attr_reader :assignment
def initialize(view, assessment_request, user)
@view = view
@assessment_request = assessment_request
@user = user
@assignment = assessment_request.asset.assignment
end
def published?
@assessment_request.asset.assignment.published?
end
def assignment_presenter
AssignmentPresenter.new(@view, @assignment, @user, :reviewing)
end
def submission_path
@view.course_assignment_submission_path(@assignment.context_id, @assignment.id, @assessment_request.user_id)
end
def ignore_url
@view.todo_ignore_api_url('reviewing', @assessment_request)
end
def ignore_title
I18n.t('Ignore this assignment')
end
def ignore_flash_message
end
def submission_author_name
@view.submission_author_name_for(@assessment_request, "#{I18n.t('user')}: ")
end
end
end

View File

@ -60,7 +60,7 @@
margin-bottom: $right_side_margin;
}
.right-side-list {
#right-side .right-side-list {
@include reset-list;
@if $use_new_styles { margin: 0 0 $ic-sp; }
@else { margin: 10px 0; }
@ -78,7 +78,7 @@
b, &.more_link {
text-decoration: underline;
}
em {
em, p {
color: #666;
}
}
@ -107,16 +107,18 @@
font-size: 1em;
}
}
b, em {
b, em, p {
font-weight: normal;
display: block;
}
em, .more_link {
font-style: italic;
p, .more_link {
@include fontSize(12px);
line-height: 13px;
}
em {
em, .more_link {
font-style: italic;
}
em, p {
@if $use_high_contrast {
color: #555;
}
@ -125,6 +127,9 @@
}
padding-bottom: 2px;
}
p {
margin-bottom: 0;
}
.more_link {
padding-top: 6px;
}
@ -139,6 +144,7 @@
.recent_feedback_comment {
color: $gray-light;
border: 0;
font-size: 100%;
}
.tooltip {
.tooltip_wrap {
@ -164,6 +170,16 @@
height: 1px;
}
}
&.to-do-list, &.events {
i {
width: 25px;
}
.todo-badge-wrapper {
display: inline-block;
vertical-align: top;
min-width: 25px;
}
}
&.to-do-list {
li {
position: relative;

View File

@ -313,6 +313,11 @@
@if $use_new_styles { color: $ic-color-success; }
@else { color: #33802a; }
}
.todo-badge {
@include ic-badge-maker(18px);
vertical-align: top;
margin-top: 2px;
}
}
div.event-details, div.todo-details {
display: inline-block;

View File

@ -1,167 +0,0 @@
<%
# This is rendered both on the dashboard and on the course homepage. On the
# dashboard, contexts is nil, and so the cache is only based on the user, which
# does not get touched when an assignment needs_grading count changes. So for
# the dashboard, we expire after 3 minutes. On the course page, contexts is the
# course, which does get touched, and so the cache expiration works.
#
# BTW if you add a new thing here, it probably needs adding to the /users/self/todo API
cache_opts = (contexts.present? ? {} : { :expires_in => 3.minutes })
cache(safe_cache_key([@current_user, contexts, 'a_need_grading']), cache_opts) do
show_context = !contexts || contexts.length > 1
visible_limit = 5
needs_grading = @current_user ? @current_user.assignments_needing_grading(:contexts => contexts, :limit => 100) : []
needs_moderation = @current_user ? @current_user.assignments_needing_moderation(:contexts => contexts, :limit => 100) : []
needs_submitting = @current_user ? @current_user.assignments_needing_submitting(:contexts => contexts, :limit => 100) : []
needs_reviewing = @current_user ? @current_user.submissions_needing_peer_review(:contexts => contexts, :limit => 100) : []
hidden_todos = 0
%>
<% if @current_user && needs_grading.present? || needs_moderation.present? || needs_submitting.present? || needs_reviewing.present? %>
<h2><%= t('headings.to_do', %{To Do}) %></h2>
<ul class="right-side-list to-do-list">
<% needs_grading.each_with_index do |assignment, i| %>
<% icon_explanation, icon_aria_label, icon_class = icon_data(context: assignment.context,
current_user: @current_user,
recent_event: assignment)
%>
<li class="todo" style="<%= hidden if i >= visible_limit %>">
<a
class="item tooltip"
href="<%= speed_grader_course_gradebook_path( assignment.context_id, :assignment_id => assignment.id) %>"
data-track-category="dashboard"
data-track-label="todo needs grading"
>
<% if assignment.due_at || assignment.points_possible || show_context %>
<span class="tooltip_wrap">
<span class='tooltip-carat'></span>
<span class="tooltip_text">
<span style="display: block;"><%= translated_due_date(assignment) %></span>
<% if assignment.points_possible %>
<span style="display: block; font-size: 0.8em;"><%= t 'points_possible', 'out of %{points_possible}', :points_possible => assignment.points_possible %></span>
<% end %>
<% if show_context %>
<span style="display: block; font-size: 0.8em;"><%= assignment.context.short_name %></span>
<% end %>
</span>
</span>
<% end %>
<i class="<%= icon_class %>" aria-label="<%= icon_aria_label %>"></i>
<div class="todo-details">
<b><%= t 'headings.grade', 'Grade %{assignment}', :assignment => assignment.title %></b>
<em>
<%= t 'need_grading_count', { one: '1 needs grading', other: '%{count} need grading' }, count: Assignments::NeedsGradingCountQuery.new(assignment, @current_user).count %>
</em>
</div>
</a>
<%= render :partial => 'shared/ignore_option_list', :locals => {:type => 'grading', :item => assignment } %>
</li>
<% end %>
<% needs_moderation.each_with_index do |assignment, i| %>
<% icon_explanation, icon_aria_label, icon_class = icon_data(context: assignment.context,
current_user: @current_user,
recent_event: assignment)
%>
<li class="todo" style="<%= hidden if i >= visible_limit %>">
<a
class="item tooltip"
href="<%= course_assignment_moderate_path(assignment.context_id, assignment) %>"
data-track-category="dashboard"
data-track-label="todo needs moderation"
>
<% if assignment.due_at || assignment.points_possible || show_context %>
<span class="tooltip_wrap">
<span class='tooltip-carat'></span>
<span class="tooltip_text">
<span style="display: block;"><%= translated_due_date(assignment) %></span>
<% if assignment.points_possible %>
<span style="display: block; font-size: 0.8em;"><%= t 'points_possible', 'out of %{points_possible}', :points_possible => assignment.points_possible %></span>
<% end %>
<% if show_context %>
<span style="display: block; font-size: 0.8em;"><%= assignment.context.short_name %></span>
<% end %>
</span>
</span>
<% end %>
<i class="<%= icon_class %>" aria-label="<%= icon_aria_label %>"></i>
<div class="todo-details">
<b><%= t 'headings.moderate', 'Moderate %{assignment}', :assignment => assignment.title %></b>
</div>
</a>
<%= render :partial => 'shared/ignore_option_list', :locals => {:type => 'moderation', :item => assignment } %>
</li>
<% end %>
<% needs_submitting.each_with_index do |assignment, i| %>
<% icon_explanation, icon_aria_label, icon_class = icon_data(context: assignment.context,
current_user: @current_user,
recent_event: assignment,
student_only: true)
%>
<li class="todo" style="<%= hidden if i >= visible_limit %>">
<a
class="item tooltip todo-tooltip"
href="<%= course_assignment_path( assignment.context_id, assignment.id ) %>#submit"
data-track-category="dashboard"
data-track-label="todo needs submitting"
>
<% if show_context || assignment.points_possible %>
<span class="tooltip_wrap">
<span class='tooltip-carat'></span>
<span class="tooltip_text">
<% if assignment.points_possible %>
<span class="screenreader_points_possible" aria-live="true" style="display: block; font-size: 0.8em;"><%= t 'points_possible', 'out of %{points_possible}', :points_possible => assignment.points_possible %></span>
<% end %>
<% if show_context %>
<span style="display: block; font-size: 0.8em;"><%= assignment.context.short_name %></span>
<% end %>
</span>
</span>
<% end %>
<i class="<%= icon_class %>" aria-label="<%= icon_aria_label %>"></i>
<div class="todo-details">
<b><%= assignment.submission_action_string %></b>
<em><%= translated_due_date(assignment) %></em>
</div>
</a>
<%= render :partial => 'shared/ignore_option_list', :locals => {:type => 'submitting', :item => assignment } %>
</li>
<% end %>
<% needs_reviewing.each_with_index do |assessment_request, i| %>
<% if assessment_request.asset.assignment.published? %>
<li class="todo" style="<%= hidden if i >= visible_limit %>">
<% assignment = assessment_request.asset.assignment %>
<a
class="item tooltip"
href="<%= course_assignment_submission_path( assignment.context_id, assignment.id, assessment_request.user_id) %>"
data-track-category="dashboard"
data-track-label="todo needs reviewing"
data-tooltip
title="<%= submission_author_name_for(assessment_request, "#{t('user')}: ") %>"
>
<i class="icon-peer-review" aria-label="Peer Review"></i>
<div class="todo-details">
<b>Peer Review for <%= assignment.title %></b>
<em>
<% if assignment.peer_reviews_due_at %>
<%= t('peer_review_due_date', 'due: %{peer_review_due_at}', {:peer_review_due_at => datetime_string(force_zone(assignment.peer_reviews_due_at))}) %>
<% else %>
<%= t('no_peer_review_due_date', 'due: No Due Date') %>
<% end %>
</em>
</div>
</a>
<%= render :partial => 'shared/ignore_option_list', :locals => {:type => 'reviewing', :item => assessment_request} %>
</li>
<% end %>
<% end %>
<% hidden_items = [needs_grading.length - visible_limit, 0].max + [needs_submitting.length - visible_limit, 0].max + [needs_reviewing.length - visible_limit, 0].max %>
<% if hidden_items > 0 %>
<li>
<a href="#" class="more_link">
<%= t 'links.show_more', '%{count} more...', :count => hidden_items %>
</a>
</li>
<% end %>
</ul>
<% end %>
<% end %>

View File

@ -9,7 +9,7 @@
if @current_user_submissions
submission = @current_user_submissions.detect { |s| s.assignment_id == recent_event.id }
elsif @current_user
submission = @current_user.submissions.select([:id, :assignment_id, :score, :workflow_state, :updated_at]).where(:assignment_id => recent_event).first
submission = @current_user.submissions.select([:id, :assignment_id, :score, :grade, :workflow_state, :updated_at]).where(:assignment_id => recent_event).first
end
if is_assignment
@ -17,7 +17,8 @@
end
end
cache([ 'recent_event_render2',
cache([ 'recent_event_render3',
@current_user, # needs to be here to bust the cache when the user changes course nicknames
submission || 'no_submission',
recent_event || 'blank_event',
(recent_event.due_at if is_assignment),
@ -32,50 +33,39 @@
:recent_event => recent_event,
:submission => submission,
:show_assignment_type_icon => true)
if is_calendar_event
context = recent_event.effective_context
else
context = recent_event.context
end
%>
<a
data-track-category="dashboard"
data-track-label="recent event"
class="tooltip" href="<%= recent_event_url(recent_event) %>"
href="<%= recent_event_url(recent_event) %>"
>
<span class="tooltip_wrap">
<span class='tooltip-carat'></span>
<span class="tooltip_text">
<% if is_assignment %>
<span style="display: block;"><%= t(:due, "due") %>:
<%= due_at(recent_event, @current_user) %>
</span>
<% if icon_explanation %>
<span style="display: block; font-style: italic;"><%= icon_explanation %></span>
<% end %>
<% if recent_event.points_possible %>
<span style="display: block;"><%= t 'submission_score', %{%{score} *out of %{points_possible}*},
:score => "<strong>#{render_score(submission.score) if submission && !recent_event.muted?}</strong>".html_safe,
:points_possible => recent_event.points_possible,
:wrapper => '<span style="font-size: 0.8em;">\1</span>' %></span>
<% end %>
<% else %>
<span style="display: block;"><%= datetime_string(recent_event.start_at, :event, recent_event.end_at) %></span>
<% end %>
<% if show_context %>
<span style="display: block; font-size: 0.8em;">
<%= is_calendar_event ? recent_event.effective_context.short_name : recent_event.context.short_name %>
</span>
<% end %>
</span>
</span>
<i class="<%= icon_class %>" aria-label="<%= icon_aria_label %>"></i>
<div class="event-details">
<b><%= recent_event.title %></b>
<em>
<% if show_context %>
<p><%= context.nickname_for(@current_user) %></p>
<% end %>
<p>
<% if is_assignment %>
<%= due_at(recent_event, @current_user, 'date') %>
<% if submission && readable_grade(submission) && !recent_event.muted? %>
<%= readable_grade(submission) %>
&bullet;
<% elsif recent_event.points_possible %>
<%= t({one: '1 point', other: '%{count} points'}, count: recent_event.points_possible) %>
&bullet;
<% end %>
<%= due_at(recent_event, @current_user) %>
<% else %>
<%= datetime_string(recent_event.start_at, :short ) %>
<%= datetime_string(recent_event.start_at) %>
<% end %>
</em>
</p>
</div>
</a>

View File

@ -7,7 +7,8 @@
grade = nil
score = nil
cache(['recent_feedback_render2', recent_feedback || 'blank_feedback', Time.zone.utc_offset].cache_key) do
# current user needs to be in the cache key to bust the cache when they change course nicknames
cache(['recent_feedback_render3', @current_user, recent_feedback || 'blank_feedback', Time.zone.utc_offset].cache_key) do
context = recent_feedback.context
assignment = recent_feedback.assignment
url = context_url(context, :context_assignment_submission_url, :assignment_id => recent_feedback.assignment_id, :id=>@current_user.id)
@ -25,42 +26,8 @@
<a
data-track-category="dashboard"
data-track-label="recent feedback"
class="recent_feedback_icon tooltip" href="<%= url %>"
class="recent_feedback_icon" href="<%= url %>"
>
<span class="tooltip_wrap">
<span class='tooltip-carat'></span>
<span class="tooltip_text">
<% if recent_feedback.workflow_state != 'unsubmitted' %>
<span style="display: block;"><%= before_label(icon_explanation.try(:capitalize)) %>
<% if recent_feedback.workflow_state == 'graded' %>
<span style="font-size: 0.8em;"><%= datetime_string(recent_feedback.graded_at) %></span>
<% else %>
<span style="font-size: 0.8em;"><%= datetime_string(recent_feedback.submitted_at) %></span>
<% end %>
</span>
<% unless recent_feedback.workflow_state == 'graded' %>
<span style="display: block; font-style: italic;"><%= t('not_graded', %{not graded}) %></span>
<% end %>
<% else %>
<span style="display: block; font-style: italic;"><%= icon_explanation.try(:capitalize) %></span>
<span style="display: block; font-size: 0.8em;"><%= translated_due_date(assignment) %></span>
<% end %>
<% if assignment.points_possible %>
<span style="display: block;">
<% if grade %>
<%= t 'submission_points.with_score', %{Score: %{score} *out of %{points_possible}*}, :score => render_score(score), :points_possible => assignment.points_possible,
:wrapper => '<span style="font-size: 0.8em;">\1</span>' %>
<% else %>
<span style="font-size: 0.8em;"><%= t 'submission_points.without_score', "out of %{points_possible}", :points_possible => assignment.points_possible %></span>
<% end %>
</span>
<% end %>
<span style="display: block;"><%= t 'comments', "Comments: %{count}", :count => recent_feedback.submission_comments.length %></span>
<% if show_context %>
<span style="display: block; font-size: 0.8em;"><%= context.short_name %></span>
<% end %>
</span>
</span>
<i class="<%= icon_class %>"></i>
<div class="event-details">
<% if comment && comment.media_comment? %>
@ -68,18 +35,14 @@
<img src="<%= kaltura_thumbnail_url %>" style="float: right; padding-left: 3px;" alt=""/>
<% end %>
<b class="recent_feedback_title"><%= recent_feedback.assignment.title %></b>
<% if comment_text or grade %>
<em class="recent_feedback_comment">
<% if grade %>
<strong><%= grade %></strong>
<%= " - " if comment_text && (!comment || !comment.media_comment?) %>
<% end %>
<% if comment_text %>
<span class="<%= 'hidden-readable' if comment && comment.media_comment? %>">
&ldquo;<%= comment_text %>&rdquo;
</span>
<% end %>
</em>
<% if show_context %>
<p><%= context.nickname_for(@current_user) %></p>
<% end %>
<% if grade %>
<p><strong><%= grade %></strong></p>
<% end %>
<% if comment_text %>
<p>"<%= comment_text %>"</p>
<% end %>
</div>
<div class="clear"></div>

View File

@ -0,0 +1,141 @@
<%
# This is rendered both on the dashboard and on the course homepage. On the
# dashboard, contexts is nil, and so the cache is only based on the user, which
# does not get touched when an assignment needs_grading count changes. So for
# the dashboard, we expire after 3 minutes. On the course page, contexts is the
# course, which does get touched, and so the cache expiration works.
#
# BTW if you add a new thing here, it probably needs adding to the /users/self/todo API
cache_opts = (contexts.present? ? {} : { :expires_in => 3.minutes })
cache(safe_cache_key([@current_user, contexts, 'to_do_list_view']), cache_opts) do
hidden_todos = 0
presenter = ToDoListPresenter.new(self, @current_user, contexts)
%>
<% if presenter.any_assignments? %>
<h2><%= t('headings.to_do', %{To Do}) %></h2>
<ul class="right-side-list to-do-list">
<% presenter.needs_grading.each_with_index do |assignment, i| %>
<li class="todo" style="<%= hidden if i >= presenter.visible_limit %>">
<a
class="item"
href="<%= assignment.gradebook_path %>"
data-track-category="dashboard"
data-track-label="todo needs grading"
>
<div class="todo-badge-wrapper">
<div class="todo-badge">
<span aria-hidden="true"><%= assignment.needs_grading_badge %></span>
<span class="screenreader-only"><%= assignment.needs_grading_label %></span>
</div>
</div>
<div class="todo-details">
<b><%= t('Grade %{assignment}', assignment: assignment.title) %></b>
<% if presenter.show_context? %>
<p><%= assignment.context_name %></p>
<% end %>
<p>
<% if assignment.points_possible %>
<%= t({one: '1 point', other: '%{count} points'}, count: assignment.points_possible) %>
&bullet;
<% end %>
<%= assignment.formatted_due_date %>
</p>
</div>
</a>
<%= render :partial => 'shared/ignore_option_list', :locals => {presenter: assignment} %>
</li>
<% end %>
<% presenter.needs_moderation.each_with_index do |assignment, i| %>
<% icon_explanation, icon_aria_label, icon_class = assignment.needs_moderation_icon_data %>
<li class="todo" style="<%= hidden if i >= presenter.visible_limit %>">
<a
class="item"
href="<%= assignment.moderate_path %>"
data-track-category="dashboard"
data-track-label="todo needs moderation"
>
<i class="<%= icon_class %>" aria-label="<%= icon_aria_label %>"></i>
<div class="todo-details">
<b><%= t('Moderate %{assignment}', assignment: assignment.title) %></b>
<% if presenter.show_context? %>
<p><%= assignment.context_name %></p>
<% end %>
<p>
<% if assignment.points_possible %>
<%= t({one: '1 point', other: '%{count} points'}, count: assignment.points_possible) %>
&bullet;
<% end %>
<%= assignment.formatted_due_date %>
</p>
</div>
</a>
<%= render :partial => 'shared/ignore_option_list', :locals => {presenter: assignment} %>
</li>
<% end %>
<% presenter.needs_submitting.each_with_index do |assignment, i| %>
<% icon_explanation, icon_aria_label, icon_class = assignment.needs_submitting_icon_data %>
<li class="todo" style="<%= hidden if i >= presenter.visible_limit %>">
<a
class="item"
href="<%= assignment.assignment_path %>#submit"
data-track-category="dashboard"
data-track-label="todo needs submitting"
>
<i class="<%= icon_class %>" aria-label="<%= icon_aria_label %>"></i>
<div class="todo-details">
<b><%= assignment.submission_action_string %></b>
<% if presenter.show_context? %>
<p><%= assignment.context_name %></p>
<% end %>
<p>
<% if assignment.points_possible %>
<%= t({one: '1 point', other: '%{count} points'}, count: assignment.points_possible) %>
&bullet;
<% end %>
<%= assignment.formatted_due_date %>
</p>
</div>
</a>
<%= render :partial => 'shared/ignore_option_list', :locals => {presenter: assignment} %>
</li>
<% end %>
<% presenter.needs_reviewing.each_with_index do |assessment_request, i| %>
<li class="todo" style="<%= hidden if i >= presenter.visible_limit %>">
<% assignment = assessment_request.assignment_presenter %>
<a
class="item"
href="<%= assessment_request.submission_path %>"
data-track-category="dashboard"
data-track-label="todo needs reviewing"
title="<%= assessment_request.submission_author_name %>"
>
<%# Don't need aria-label here because we say that this is a peer review in the text immediately following %>
<i class="icon-peer-review"></i>
<div class="todo-details">
<b><%= t('Peer Review for %{assignment}', assignment: assignment.title) %></b>
<% if presenter.show_context? %>
<p><%= assessment_request.context_name %></p>
<% end %>
<p>
<% if assignment.try(:points_possible) %>
<%= t({one: '1 point', other: '%{count} points'}, count: assignment.points_possible) %>
&bullet;
<% end %>
<%= assignment.formatted_peer_review_due_date %>
</p>
</div>
</a>
<%= render :partial => 'shared/ignore_option_list', :locals => {presenter: assessment_request} %>
</li>
<% end %>
<% if presenter.hidden_count > 0 %>
<li>
<a href="#" class="more_link">
<%= t 'links.show_more', '%{count} more...', :count => presenter.hidden_count %>
</a>
</li>
<% end %>
</ul>
<% end %>
<% end %>

View File

@ -97,7 +97,7 @@
</div>
</div>
<% end %>
<%= render :partial => 'assignments_needing_grading', :locals => {:contexts => [@context]} %>
<%= render :partial => 'to_do_list', :locals => {:contexts => [@context]} %>
<%= render :partial => "group_list", :locals => {:group_list => @user_groups} %>
<%= nbsp unless @current_user %>
</div>

View File

@ -1,30 +1,12 @@
<%# presenter is a ToDoListPresenter::AssignmentPresenter or a ToDoListPresenter::AssessmentRequestPresenter %>
<div class="IgnoreButton al-dropbown__container">
<a class="<%= todo_link_classes(type) %>"
aria-haspopup="true"
title="<%= t('Ignore this assignment') %>"
<a class="disable_item_link disable-todo-item-link"
title="<%= presenter.ignore_title %>"
href="#"
data-api-href="<%= todo_ignore_api_url(type, item) %>"
data-popup-within="body"
data-api-href="<%= presenter.ignore_url %>"
data-flash-message="<%= presenter.ignore_flash_message %>"
>
<i class="icon-x"></i>
<span class="screenreader-only"><%= t('Ignore') %></span>
</a>
<% if todo_ignore_dropdown_type?(type) %>
<% title = case type.to_sym
when :grading
t('Ignore Until New Submission')
when :moderation
t('Ignore Until New Mark')
end %>
<ul id="ignore_dropdown" class="al-options" role="menu" tabindex="0" aria-hidden="true" aria-expanded="false" aria-activedescendant="ignore_dropdown">
<li role="presentation">
<a href="#" id="ignore_forever" class="icon-trash disable-todo-item-link" tabindex="-1" role="menuitem" data-api-href="<%= todo_ignore_api_url(type, item, true) %>" title="<%= t('Ignore') %>"><%= t('Ignore') %></a>
</li>
<li role="presentation">
<a href="#" id="ignore_until_submission" class="icon-star disable-todo-item-link" tabindex="-1" role="menuitem" data-api-href="<%= todo_ignore_api_url(type, item) %>" title="<%= title %>"><%= title %></a>
</li>
</ul>
<% end %>
</div>

View File

@ -5,7 +5,7 @@
</div>
<% end %>
<%= render :partial => 'courses/assignments_needing_grading', :locals => {:contexts => nil} %>
<%= render :partial => 'courses/to_do_list', :locals => {:contexts => nil} %>
<% locals = {
:title => t('coming_up', "Coming Up"),
:period => :one_week,

View File

@ -853,14 +853,18 @@ define([
event.preventDefault();
var $item = $(this).parents("li, div.topic_message").last();
var $prevItem = $(this).closest('.to-do-list > li').prev()
var toFocus = ($prevItem.find('.al-trigger').length && $prevItem.find('.al-trigger')) ||
var toFocus = ($prevItem.find('.disable-todo-item-link').length && $prevItem.find('.disable-todo-item-link')) ||
$('.event-list-view-calendar')
var url = $(this).data('api-href');
var flashMessage = $(this).data('flash-message');
function remove(delete_url) {
$item.confirmDelete({
url: delete_url,
noMessage: true,
success: function() {
if (flashMessage) {
$.flashMessage(flashMessage);
}
$(this).slideUp(function() {
$(this).remove();
toFocus.focus();

View File

@ -27,7 +27,7 @@ describe "varied due dates" do
def assert_coming_up_due_date(response, expected)
doc = Nokogiri::HTML(response.body)
expect(doc.at_css("#right-side .coming_up .event a .tooltip_text").text).to include(
expect(doc.at_css("#right-side .coming_up .event a .event-details").text).to include(
expected.is_a?(String) ? expected : datetime_string(expected)
)
end
@ -41,7 +41,7 @@ describe "varied due dates" do
def assert_recent_feedback_due_date(response, expected)
doc = Nokogiri::HTML(response.body)
expect(doc.at_css("#right-side .recent_feedback .event a .tooltip_text").text).to include(
expect(doc.at_css("#right-side .recent_feedback .event a .event-details").text).to include(
expected.is_a?(String) ? expected : datetime_string(expected)
)
end
@ -151,13 +151,6 @@ describe "varied due dates" do
get '/dashboard-sidebar'
assert_coming_up_due_date wrap_partial(response), @course_due_date
end
it "shows the course due date in 'recent feedback'" do
create_recent_feedback @student1
login_as(@student1.pseudonym.login, 'asdfasdf')
get '/dashboard-sidebar'
assert_recent_feedback_due_date wrap_partial(response), @course_due_date
end
end
context "in the overridden section" do
@ -173,13 +166,6 @@ describe "varied due dates" do
get '/dashboard-sidebar'
assert_coming_up_due_date wrap_partial(response), @section_due_date
end
it "shows the section due date in 'recent feedback'" do
create_recent_feedback @student2
login_as(@student2.pseudonym.login, 'asdfasdf')
get '/dashboard-sidebar'
assert_recent_feedback_due_date wrap_partial(response), @section_due_date
end
end
end

View File

@ -48,12 +48,13 @@ describe "dashboard" do
due_date = Time.now.utc + 2.days
@assignment = assignment_model({:due_at => due_date, :course => @course})
get "/"
expect(f('.events_list .event a')).to include_text(@assignment.title)
event = f('.events_list .event a')
expect(event).to include_text(@assignment.title)
# use jQuery to get the text since selenium can't figure it out when the elements aren't displayed
expect(driver.execute_script("return $('.event a .tooltip_text').text()")).to match(@course.short_name)
expect(event).to include_text(@course.short_name)
end
it "should display quiz submissions with essay questions as submitted in coming up list", priority: "1", test_id: 216395 do
it "should display quiz submissions with essay questions with points in coming up list", priority: "1", test_id: 216395 do
quiz_with_graded_submission([:question_data => {:id => 31,
:name => "Quiz Essay Question 1",
:question_type => 'essay_question',
@ -70,9 +71,8 @@ describe "dashboard" do
@assignment.save!
get "/"
keep_trying_until { expect(ffj(".events_list .event .tooltip_wrap").size).to be > 0 }
driver.execute_script("$('.events_list .event .tooltip_wrap, .events_list .event .tooltip_text').css('visibility', 'visible')")
expect(f('.events_list .event .tooltip_wrap')).to include_text 'submitted'
keep_trying_until { expect(ffj(".events_list .event-details").size).to be > 0 }
expect(f('.events_list .event-details')).to include_text '10 points'
end
end
end
end

View File

@ -39,41 +39,6 @@ describe "dashboard" do
end
end
it "should be able to ignore an assignment to grade permanently", priority: "1", test_id: 216398 do
assignment = assignment_model({:submission_types => 'online_text_entry', :course => @course})
student = user_with_pseudonym(:active_user => true, :username => 'student@example.com', :password => 'qwerty')
student2 = user_with_pseudonym(:active_user => true, :username => 'student2@example.com', :password => 'qwerty')
@course.enroll_user(student, "StudentEnrollment", :enrollment_state => 'active')
@course.enroll_user(student2, "StudentEnrollment", :enrollment_state => 'active')
assignment.reload
assignment.submit_homework(student, {:submission_type => 'online_text_entry', :body => 'ABC'})
assignment.reload
enable_cache do
get "/"
f('.to-do-list .disable_item_link').click
wait_for_ajaximations
f('#ignore_forever').click
wait_for_ajaximations
expect(f('.to-do-list > li')).to be_nil
get "/"
expect(f('.to-do-list')).to be_nil
end
assignment.reload
assignment.submit_homework(student2, {:submission_type => 'online_text_entry', :body => 'ABC'})
assignment.reload
enable_cache do
get "/"
expect(f('.to-do-list')).to be_nil
end
end
it "should be able to ignore an assignment until the next submission", priority: "1", test_id: 216399 do
assignment = assignment_model({:submission_types => 'online_text_entry', :course => @course})
student = user_with_pseudonym(:active_user => true, :username => 'student@example.com', :password => 'qwerty')
@ -86,10 +51,8 @@ describe "dashboard" do
enable_cache do
get "/"
f('.to-do-list .disable_item_link').click
wait_for_ajaximations
ignore_link = f('#ignore_until_submission')
expect(ignore_link).to include_text("Ignore Until New Submission")
ignore_link = f('.to-do-list .disable_item_link')
expect(ignore_link['title']).to include_text("Ignore until new submission")
ignore_link.click
wait_for_ajaximations
expect(f('.to-do-list > li')).to be_nil
@ -162,12 +125,9 @@ describe "dashboard" do
get "/"
ff('.to-do-list .disable_item_link').each do |link|
expect(link['title']).to include_text("Ignore until new mark")
link.click
wait_for_ajaximations
ignore_link = f('#ignore_until_submission')
expect(ignore_link).to include_text("Ignore Until New Mark")
ignore_link.click
wait_for_ajaximations
end
expect(f('.to-do-list > li')).to be_nil
@ -203,8 +163,6 @@ describe "dashboard" do
all_todo_links = ff('.to-do-list .disable_item_link')
all_todo_links.last.click
wait_for_ajaximations
ff('#ignore_forever').last.click
wait_for_ajaximations
check_element_has_focus(all_todo_links.first)
end
@ -216,8 +174,6 @@ describe "dashboard" do
f('.to-do-list .disable_item_link').click
wait_for_ajaximations
f('#ignore_forever').click
wait_for_ajaximations
check_element_has_focus(f('.event-list-view-calendar'))
end

View File

@ -38,14 +38,60 @@ describe "/courses/_recent_event" do
expect(response.body).to match %r{<b>my assignment</b>}
end
context "assignment muting and tooltips" do
it "shows the context when asked to" do
course_with_student_logged_in
event = @course.calendar_events.create(title: "some assignment", start_at: Time.zone.now)
render partial: "courses/recent_event", object: event, locals: {is_hidden: false, show_context: true}
expect(response.body).to include(@course.name)
end
it "doesn't show the context when not asked to" do
course_with_student_logged_in
event = @course.calendar_events.create(title: "some assignment", start_at: Time.zone.now)
render partial: "courses/recent_event", object: event, locals: {is_hidden: false}
expect(response.body).to_not include(@course.name)
end
context 'assignments' do
before do
course_with_student(active_all: true)
submission_model
assigns[:current_user] = @user
end
it 'shows points possible for an ungraded assignment' do
render partial: "courses/recent_event", object: @assignment, locals: {is_hidden: false}
expect(response.body).to include("#{@assignment.points_possible} points")
end
it 'shows the grade for a graded assignment' do
@assignment.grade_student(@user, grade: 7)
render partial: "courses/recent_event", object: @assignment, locals: {is_hidden: false}
expect(response.body).to include("7 out of #{@assignment.points_possible}")
end
it 'shows the due date' do
render partial: "courses/recent_event", object: @assignment, locals: {is_hidden: false}
expect(response.body).to include(view.datetime_string(@assignment.due_at))
end
end
context "assignment muting" do
before(:each) do
course_with_student
view_context
@quiz = @course.quizzes.create!
@quiz.generate_quiz_data
@quiz.workflow_state = 'available'
@quiz.published_at = Time.now
@quiz.published_at = Time.zone.now
@quiz.save
expect(@quiz.assignment).not_to be_nil
@ -53,18 +99,18 @@ describe "/courses/_recent_event" do
Quizzes::SubmissionGrader.new(@quiz_submission).grade_submission
@submission = @quiz_submission.submission
Submission.any_instance.stubs(:score).returns(1234567890987654400)
Submission.any_instance.stubs(:grade).returns(1234567890987654400)
end
it "should show the score for a non-muted assignment" do
it "should show the grade for a non-muted assignment" do
render :partial => "courses/recent_event", :object => @quiz.assignment, :locals => { :is_hidden => false, :submissions => [ @submission ] }
expect(response.body).to match /#{@submission.score}/
expect(response.body).to match /#{@submission.grade}/
end
it "should not show the score for a muted assignment" do
it "should not show the grade for a muted assignment" do
@quiz.assignment.mute!
render :partial => "courses/recent_event", :object => @quiz.assignment, :locals => { :is_hidden => false, :submissions => [ @submission ] }
expect(response.body).not_to match /#{@submission.score}/
expect(response.body).not_to match /#{@submission.grade}/
end
end
end

View File

@ -0,0 +1,56 @@
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
require File.expand_path(File.dirname(__FILE__) + '/../views_helper')
describe "/courses/_recent_feedback" do
before do
course_with_student(active_all: true)
assigns[:current_user] = @user
submission_model
end
it 'shows the context when asked to' do
@assignment.grade_student(@user, grade: 7)
@submission.reload
render partial: "courses/recent_feedback", object: @submission, locals: {is_hidden: false, show_context: true}
expect(response.body).to include(@course.name)
end
it "doesn't show the context when not asked to" do
@assignment.grade_student(@user, grade: 7)
@submission.reload
render partial: "courses/recent_feedback", contexts: [@course], object: @submission, locals: {is_hidden: false}
expect(response.body).to_not include(@course.name)
end
it 'shows the comment' do
@assignment.grade_student(@user, comment: 'bunch of random stuff', grader: @teacher)
@submission.reload
render partial: "courses/recent_feedback", object: @submission, locals: {is_hidden: false}
expect(response.body).to include('bunch of random stuff')
end
it 'shows the grade' do
@assignment.grade_student(@user, grade: 5782394)
@submission.reload
render :partial => "courses/recent_feedback", object: @submission, locals: {is_hidden: false}
expect(response.body).to include("5782394 out of #{@assignment.points_possible}")
end
it 'shows the grade and the comment' do
@assignment.grade_student(@user, grade: 25734, comment: 'something different', grader: @teacher)
@submission.reload
render :partial => "courses/recent_feedback", object: @submission, locals: {is_hidden: false}
expect(response.body).to include("25734 out of #{@assignment.points_possible}")
expect(response.body).to include('something different')
end
end

View File

@ -0,0 +1,125 @@
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
require File.expand_path(File.dirname(__FILE__) + '/../views_helper')
describe "courses/_to_do_list.html.erb" do
include AssignmentsHelper
context "as a student" do
describe "assignments due" do
it "shows assignment data" do
course_with_student(active_all: true)
@user.course_nicknames[@course.id] = "My Awesome Course"
@user.save!
due_date = 2.days.from_now
assignment_model(course: @course,
due_at: due_date,
submission_types: "online_text_entry",
points_possible: 15,
title: "SubmitMe")
view_context
# title, course nickname, points, due date
render partial: "courses/to_do_list", locals: {contexts: nil}
expect(response).to include "Turn in SubmitMe"
expect(response).to include "15 points"
expect(response).to include "My Awesome Course"
expect(response).to include due_at(@assignment, @user)
end
end
describe "submissions to review" do
it "shows peer reviews" do
course(active_all: true)
due_date = 2.days.from_now
assignment_model(course: @course,
due_at: due_date,
submission_types: "online_text_entry",
points_possible: 15,
title: "ReviewMe",
peer_reviews: true)
@submission = submission_model(assignment: @assignment, body: "my submission")
@submitter = @user
@assessor_submission = submission_model(assignment: @assignment, user: @user, body: "my other submission")
@assessor = @user
@assessor.course_nicknames[@course.id] = "My Awesome Course"
@assessor.save!
@assessment_request = AssessmentRequest.create!(assessor: @assessor, asset: @submission, user: @submitter, assessor_asset: @assessor_submission)
@assessment_request.workflow_state = "assigned"
@assessment_request.save!
view_context
render partial: "courses/to_do_list", locals: {contexts: nil}
expect(response).to include "Peer Review for ReviewMe"
end
end
end
context "as a teacher" do
describe "assignments to grade" do
it "shows assignment data" do
course_with_student(active_all: true)
due_date = 2.days.from_now
assignment_model(course: @course,
due_at: due_date,
submission_types: "online_text_entry",
points_possible: 15,
title: "GradeMe",
needs_grading_count: 7)
@user = @teacher
@user.course_nicknames[@course.id] = "My Awesome Course"
@user.save!
view_context
# title, course nickname, points, due date, number of submissions to grade
render partial: "courses/to_do_list", locals: {contexts: nil}
expect(response).to include "Grade GradeMe"
expect(response).to include "15 points"
expect(response).to include "My Awesome Course"
expect(response).to include due_at(@assignment, @user)
expect(response).to include "7"
expect(response).to include "7 submissions need grading"
end
it "shows 9+ when there are more than 9 to grade" do
course_with_student(active_all: true)
due_date = 2.days.from_now
assignment_model(course: @course,
due_at: due_date,
submission_types: "online_text_entry",
points_possible: 15,
title: "GradeMe",
needs_grading_count: 10)
@user = @teacher
@user.course_nicknames[@course.id] = "My Awesome Course"
@user.save!
view_context
# title, course nickname, points, due date, number of submissions to grade
render partial: "courses/to_do_list", locals: {contexts: nil}
expect(response).to include "Grade GradeMe"
expect(response).to include "15 points"
expect(response).to include "My Awesome Course"
expect(response).to include due_at(@assignment, @user)
expect(response).to include "9+"
expect(response).to include "More than 9 submissions need grading"
end
end
describe "assignments to moderate" do
it "shows assignment data" do
course_with_student(active_all: true)
due_date = 2.days.from_now
assignment_model(course: @course,
due_at: due_date,
submission_types: "online_text_entry",
points_possible: 15,
title: "ModerateMe",
moderated_grading: true,
needs_grading_count: 1)
@submission = submission_model(assignment: @assignment, body: "my submission")
@submission.find_or_create_provisional_grade!(scorer: @teacher, grade: 5)
@user = @teacher
@user.course_nicknames[@course.id] = "My Awesome Course"
@user.save!
view_context
render partial: "courses/to_do_list", locals: {contexts: nil}
expect(response).to include "Moderate ModerateMe"
end
end
end
end