Commit 69b5b033 by Julia Hansbrough

Midcourse reverifications: dismissal banner

Added a banner that students can use to dismiss the "you have failed reverification" message.  Also, removed unused code, wired up the sidebar for design
parent 436a1ec7
......@@ -57,14 +57,15 @@ class TestMidcourseReverificationWindow(TestCase):
)
def test_no_overlapping_windows(self):
MidcourseReverificationWindowFactory(
window_valid = MidcourseReverificationWindow(
course_id=self.course_id,
start_date=datetime.now(pytz.utc) - timedelta(days=3),
end_date=datetime.now(pytz.utc) + timedelta(days=3)
)
window_valid.save()
with self.assertRaises(ValidationError):
window_invalid = MidcourseReverificationWindowFactory(
window_invalid = MidcourseReverificationWindow(
course_id=self.course_id,
start_date=datetime.now(pytz.utc) - timedelta(days=2),
end_date=datetime.now(pytz.utc) + timedelta(days=4)
......
......@@ -84,7 +84,7 @@ log = logging.getLogger("edx.student")
AUDIT_LOG = logging.getLogger("audit")
Article = namedtuple('Article', 'title url author image deck publication publish_date')
ReverifyInfo = namedtuple('ReverifyInfo', 'course_id course_name course_number date status')
ReverifyInfo = namedtuple('ReverifyInfo', 'course_id course_name course_number date status display') # pylint: disable=C0103
def csrf_token(context):
......@@ -203,18 +203,17 @@ def reverification_info(course_enrollment_pairs, user, statuses):
reverifications = defaultdict(list)
for (course, enrollment) in course_enrollment_pairs:
info = single_course_reverification_info(user, course, enrollment)
for status in statuses:
if info and (status in info):
reverifications[status].append(info)
if info:
reverifications[info.status].append(info)
# Sort the data by the reverification_end_date
for status in statuses:
if reverifications[status]:
reverifications[status] = sorted(reverifications[status], key=lambda x: x.date)
reverifications[status].sort(key=lambda x: x.date)
return reverifications
def single_course_reverification_info(user, course, enrollment):
def single_course_reverification_info(user, course, enrollment): # pylint: disable=invalid-name
"""Returns midcourse reverification-related information for user with enrollment in course.
If a course has an open re-verification window, and that user has a verified enrollment in
......@@ -226,7 +225,7 @@ def single_course_reverification_info(user, course, enrollment):
enrollment (CourseEnrollment): the object representing the type of enrollment user has in course
Returns:
5-namedtuple: (course_id, course_name, course_number, date, status)
ReverifyInfo: (course_id, course_name, course_number, date, status)
OR, None: None if there is no re-verification info for this enrollment
"""
window = MidcourseReverificationWindow.get_window(course.id, datetime.datetime.now(UTC))
......@@ -238,6 +237,7 @@ def single_course_reverification_info(user, course, enrollment):
course.id, course.display_name, course.number,
window.end_date.strftime('%B %d, %Y %X %p'),
SoftwareSecurePhotoVerification.user_status(user, window)[0],
SoftwareSecurePhotoVerification.display_status(user, window),
)
......@@ -470,6 +470,10 @@ def dashboard(request):
except ExternalAuthMap.DoesNotExist:
pass
# If there are *any* denied reverifications that have not been toggled off,
# we'll display the banner
denied_banner = any(item.display for item in reverifications["denied"])
context = {'course_enrollment_pairs': course_enrollment_pairs,
'course_optouts': course_optouts,
'message': message,
......@@ -484,6 +488,7 @@ def dashboard(request):
'verification_status': verification_status,
'verification_msg': verification_msg,
'show_refund_option_for': show_refund_option_for,
'denied_banner': denied_banner,
}
return render_to_response('dashboard.html', context)
......
......@@ -456,9 +456,15 @@ def course_info(request, course_id):
masq = setup_masquerade(request, staff_access) # allow staff to toggle masquerade on info page
reverifications = fetch_reverify_banner_info(request, course_id)
context = {'request': request, 'course_id': course_id, 'cache': None,
'course': course, 'staff_access': staff_access, 'masquerade': masq,
'reverifications': reverifications, }
context = {
'request': request,
'course_id': course_id,
'cache': None,
'course': course,
'staff_access': staff_access,
'masquerade': masq,
'reverifications': reverifications,
}
return render_to_response('courseware/info.html', context)
......@@ -682,10 +688,7 @@ def fetch_reverify_banner_info(request, course_id):
course = course_from_id(course_id)
info = single_course_reverification_info(user, course, enrollment)
if info:
if "must_reverify" in info:
reverifications["must_reverify"].append(info)
elif "denied" in info:
reverifications["denied"].append(info)
reverifications[info.status].append(info)
return reverifications
@login_required
......
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'SoftwareSecurePhotoVerification.display'
db.add_column('verify_student_softwaresecurephotoverification', 'display',
self.gf('django.db.models.fields.BooleanField')(default=True, db_index=True),
keep_default=False)
def backwards(self, orm):
# Deleting field 'SoftwareSecurePhotoVerification.display'
db.delete_column('verify_student_softwaresecurephotoverification', 'display')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'reverification.midcoursereverificationwindow': {
'Meta': {'object_name': 'MidcourseReverificationWindow'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'end_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'start_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'})
},
'verify_student.softwaresecurephotoverification': {
'Meta': {'ordering': "['-created_at']", 'object_name': 'SoftwareSecurePhotoVerification'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
'display': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
'error_code': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
'error_msg': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'face_image_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'photo_id_image_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}),
'photo_id_key': ('django.db.models.fields.TextField', [], {'max_length': '1024'}),
'receipt_id': ('django.db.models.fields.CharField', [], {'default': "'<function uuid4 at 0x3176410>'", 'max_length': '255', 'db_index': 'True'}),
'reviewing_service': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'reviewing_user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'photo_verifications_reviewed'", 'null': 'True', 'to': "orm['auth.User']"}),
'status': ('model_utils.fields.StatusField', [], {'default': "'created'", 'max_length': '100', u'no_check_for_status': 'True'}),
'status_changed': ('model_utils.fields.MonitorField', [], {'default': 'datetime.datetime.now', u'monitor': "u'status'"}),
'submitted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
'window': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['reverification.MidcourseReverificationWindow']", 'null': 'True'})
}
}
complete_apps = ['verify_student']
\ No newline at end of file
......@@ -149,6 +149,11 @@ class PhotoVerification(StatusModel):
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
updated_at = models.DateTimeField(auto_now=True, db_index=True)
# Indicates whether or not a user wants to see the verification status
# displayed on their dash. Right now, only relevant for allowing students
# to "dismiss" a failed midcourse reverification message
display = models.BooleanField(db_index=True, default=True)
######################## Fields Set When Submitting ########################
submitted_at = models.DateTimeField(null=True, db_index=True)
......@@ -223,10 +228,9 @@ class PhotoVerification(StatusModel):
window is anything else, this will check for the reverification associated
with that window.
"""
if window:
valid_statuses = ['submitted', 'approved']
else:
valid_statuses = ['must_retry', 'submitted', 'approved']
valid_statuses = ['submitted', 'approved']
if not window:
valid_statuses.append('must_retry')
return cls.objects.filter(
user=user,
status__in=valid_statuses,
......@@ -465,6 +469,28 @@ class PhotoVerification(StatusModel):
self.status = "must_retry"
self.save()
@classmethod
def display_off(cls, user_id):
"""
Find all failed PhotoVerifications for a user, and sets those verifications' `display`
property to false, so the notification banner can be switched off.
"""
user = User.objects.get(id=user_id)
cls.objects.filter(user=user, status="denied").exclude(window=None).update(display=False)
@classmethod
def display_status(cls, user, window):
"""
Finds the `display` property for the PhotoVerification associated with
(user, window). Default is True
"""
attempts = cls.objects.filter(user=user, window=window).order_by('-updated_at')
try:
attempt = attempts[0]
return attempt.display
except IndexError:
return True
class SoftwareSecurePhotoVerification(PhotoVerification):
"""
......@@ -518,7 +544,7 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
"""
all_windows = MidcourseReverificationWindow.objects.filter(course_id=course_id)
# if there are no windows for a course, then return True right off
if (not all_windows):
if (not all_windows.exists()):
return True
for window in all_windows:
......
......@@ -381,6 +381,18 @@ class TestPhotoVerification(TestCase):
reverify_status = SoftwareSecurePhotoVerification.user_status(user=user, window=window)
self.assertEquals(reverify_status, ('denied', ''))
def test_display(self):
user = UserFactory.create()
window = MidcourseReverificationWindowFactory()
attempt = SoftwareSecurePhotoVerification(user=user, window=window, status="denied")
attempt.save()
# We expect the verification to be displayed by default
self.assertEquals(SoftwareSecurePhotoVerification.display_status(user, window), True)
# Turn it off
SoftwareSecurePhotoVerification.display_off(user.id)
self.assertEquals(SoftwareSecurePhotoVerification.display_status(user, window), False)
def test_parse_error_msg_success(self):
user = UserFactory.create()
......
......@@ -70,4 +70,10 @@ urlpatterns = patterns(
views.reverification_window_expired,
name="verify_student_reverification_window_expired"
),
url(
r'^toggle_failed_banner_off$',
views.toggle_failed_banner_off,
name="verify_student_toggle_failed_banner_off"
),
)
......@@ -416,10 +416,20 @@ def midcourse_reverify_dash(request):
context = {
"user_full_name": user.profile.name,
'reverifications': reverifications,
'referer': request.META.get('HTTP_REFERER'),
}
return render_to_response("verify_student/midcourse_reverify_dash.html", context)
def toggle_failed_banner_off(request):
"""
Finds all denied midcourse reverifications for a user and permanently toggles
the "Reverification Failed" banner off for those verifications.
"""
user_id = request.POST.get('user_id')
SoftwareSecurePhotoVerification.display_off(user_id)
@login_required
def reverification_submission_confirmation(_request):
"""
......@@ -429,7 +439,7 @@ def reverification_submission_confirmation(_request):
@login_required
def midcourse_reverification_confirmation(request): # pylint: disable=W0613
def midcourse_reverification_confirmation(_request): # pylint: disable=C0103
"""
Shows the user a confirmation page if the submission to SoftwareSecure was successful
"""
......
......@@ -23,6 +23,15 @@
$(this).closest('.message.is-expandable').toggleClass('is-expanded');
}
$("#failed-verification-button-dismiss").click(function(event) {
$.ajax({
url: "${reverse('verify_student_toggle_failed_banner_off')}",
type: "post",
data: { 'user_id': ${user.id}, }
})
$("#failed-verification-banner").css("display","none");
})
$("#upgrade-to-verified").click(function(event) {
user = $(event.target).data("user");
course = $(event.target).data("course-id");
......@@ -152,7 +161,6 @@
</script>
</%block>
<!-- TODO later will need to make this ping for all courses on the dash -->
% if reverifications["must_reverify"] or reverifications["denied"]:
<section class="dashboard-banner">
<%include file='dashboard/_dashboard_prompt_midcourse_reverify.html' />
......
......@@ -58,12 +58,13 @@
%endif
%endif
%if reverifications["denied"]:
<div class="wrapper-msg urgency-low">
%if reverifications["denied"] and denied_banner:
<div class="wrapper-msg urgency-low" id="failed-verification-banner">
<div class="msg msg-reverify is-dismissable">
<div class="msg-content">
<h2 class="title">${_("Your re-verification failed")}</h2>
% for item in reverifications["denied"]:
% if item.display:
<div class="copy">
<p class='activation-message'>
${_('Your re-verification for <strong>{course_name}</strong> failed and you are no longer eligible for a Verified Certificate. If you think this is in error, please contact us at support@edx.org.').format(course_name=item.course_name)}
......@@ -71,10 +72,11 @@
</div>
</div>
<div class="action-dismiss">
<button class="button-dismiss"><i class="icon-remove-sign"></i> <span class="sr">Dismiss</span></button>
<button class="button-dismiss" id="failed-verification-button-dismiss"><i class="icon-remove-sign"></i> <span class="sr">Dismiss</span></button>
</div>
</div>
</div>
</div>
% endif
% endfor
%endif
......@@ -11,25 +11,25 @@
% if reverifications["must_reverify"]:
% for item in reverifications["must_reverify"]:
<li class="status-item is-open">${_('<i class="icon-circle-blank"></i><span class="label">Re-verify now:</span> <span class="course-name"><a href="">{course_name}</span></a>').format(course_name=item.course_name)}</li>
<li class="status-item is-open"><i class="icon-circle-blank"></i><span class="label">${_('Re-verify now:')}</span> <span class="course-name"><a href="${reverse('verify_student_midcourse_reverify_dash')}">${item.course_name}</span></a></li>
% endfor
%endif
% if reverifications["pending"]:
% for item in reverifications["pending"]:
<li class="status-item is-pending">${_('<i class="icon-circle-blank"></i><span class="label">Pending:</span> <span class="course-name"><a href="">{course_name}</span></a>').format(course_name=item.course_name)}</li>
<li class="status-item is-pending"><i class="icon-circle-blank"></i><span class="label">${_('Pending:')}</span> <span class="course-name"><a href="${reverse('verify_student_midcourse_reverify_dash')}">${item.course_name}</span></a></li>
% endfor
%endif
% if reverifications["denied"]:
% for item in reverifications["denied"]:
<li class="status-item is-denied">${_('<i class="icon-remove-sign"></i><span class="label">Denied:</span> <span class="course-name"><a href="">{course_name}</span></a>').format(course_name=item.course_name)}</li>
<li class="status-item is-denied"><i class="icon-remove-sign"></i><span class="label">${_('Denied:')}</span> <span class="course-name"><a href="${reverse('verify_student_midcourse_reverify_dash')}">${item.course_name}</span></a></li>
% endfor
%endif
% if reverifications["approved"]:
% for item in reverifications["approved"]:
<li class="status-item is-approved">${_('<i class="icon-ok"></i><span class="label">Approved:</span> <span class="course-name"><a href="">{course_name}</span></a>').format(course_name=item.course_name)}</li>
<li class="status-item is-approved"><i class="icon-ok"></i><span class="label">${_('Approved:')}</span> <span class="course-name"><a href="${reverse('verify_student_midcourse_reverify_dash')}">${item.course_name}</span></a></li>
% endfor
%endif
</ul>
......
......@@ -27,7 +27,7 @@
<div class="actions-next">
<ol class="list-nav">
<li class="nav-item conditional">
<a class="action action-primary" href="">${_("Complete your other re-verifications")}</a>
<a class="action action-primary" href="${reverse('verify_student_midcourse_reverify_dash')}">${_("Complete your other re-verifications")}</a>
</li>
<li class="nav-item">
<a class="action" href="${reverse('dashboard')}">${_("Return to where you left off")}</a>
......
......@@ -18,12 +18,12 @@
<div class="copy">
% if reverifications["must_reverify"]:
% if len(reverifications["must_reverify"]) > 1:
% if reverifications["must_reverify"]: # if 1st IF YOU HAVE REVERIFIES
% if len(reverifications["must_reverify"]) > 1: # if 2nd IF YOU HAVE >1 REVERIFIES
<div class="wrapper-reverify-open">
<h3 class="title">${_("You currently need to re-verify for the following courses:")}</h3>
<ul class="reverification-list">
% for course_id, course_name, course_number, date, status in reverifications["must_reverify"]:
% for course_id, course_name, course_number, date, status, display in reverifications["must_reverify"]: # for 1st
<li class="item">
<div class="course-info">
<h3 class="course-name">${course_name} (${course_number})</h3>
......@@ -31,16 +31,16 @@
</div>
<p class="reverify-status"><a class="btn action-primary action-reverify" href="${reverse('verify_student_midcourse_reverify', kwargs={'course_id': course_id})}">Re-verify for ${course_number}</a></p>
</li>
% endfor
% endfor # closed for 1st
</ul>
</div>
% elif reverifications["must_reverify"]:
% elif reverifications["must_reverify"]: # con't 2nd if
<div class="wrapper-reverify-open">
<h3 class="title">${_("You currently need to re-verify for the following course:")}</h3>
<ul class="reverification-list">
% for course_id, course_name, course_number, date, status in reverifications["must_reverify"]:
% for course_id, course_name, course_number, date, status, display in reverifications["must_reverify"]:
<li class="item">
<div class="course-info">
<h3 class="course-name">${course_name} (${course_number})</h3>
......@@ -51,14 +51,19 @@
% endfor
</ul>
</div>
%endif
%endif #close 2nd if
% else:
<div class="wrapper-reverify-open">
<p class="title">${_("You have no re-verifications at present.")}</p>
</div>
%endif #close first if
% if reverifications["pending"] or reverifications["approved"] or reverifications["denied"]:
% if reverifications["pending"] or reverifications["approved"] or reverifications["denied"]: #open a 1st if
<div class="wrapper-reverify-status">
<h3 class="title">${_("The status of your submitted re-verifications:")}</h3>
<ul class="reverification-list reverification-status">
% for course_id, course_name, course_number, date, status in reverifications["pending"]:
% for course_id, course_name, course_number, date, status, display in reverifications["pending"]:
<li class="item pending">
<div class="course-info">
<h3 class="course-name">${course_name} (${course_number})</h3>
......@@ -68,7 +73,7 @@
</li>
% endfor
% for course_id, course_name, course_number, date, status in reverifications["approved"]:
% for course_id, course_name, course_number, date, status, display in reverifications["approved"]:
<li class="item complete">
<div class="course-info">
<h3 class="course-name">${course_name} (${course_number})</h3>
......@@ -78,7 +83,7 @@
</li>
% endfor
% for course_id, course_name, course_number, date, status in reverifications["denied"]:
% for course_id, course_name, course_number, date, status, display in reverifications["denied"]:
<li class="item failed">
<div class="course-info">
<h3 class="course-name">${course_name} (${course_number})</h3>
......@@ -90,14 +95,12 @@
</ul>
</div>
% endif
% else:
<p class="title">${_("You have no re-verifications at present.")}</p>
% endif
% endif # close a 1st if
% if reverifications["must_reverify"]:
<p class="support">Don't want to re-verify right now? <a href="">Return to where you left off <i class="icon-angle-right"></i></a></p>
<p class="support">Don't want to re-verify right now? <a href="${referer}">Return to where you left off <i class="icon-angle-right"></i></a></p>
% else:
<p class="support"><a href="${referer}">Return to where you left off <i class="icon-angle-right"></i></a></p>
% endif
</div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment