From bf112f7ef02bccf51c249fa47fd3bc206670a506 Mon Sep 17 00:00:00 2001 From: Stephen Sanchez <steve@edx.org> Date: Mon, 29 Sep 2014 15:47:39 +0000 Subject: [PATCH] Add new enrollment message to the dashboard The body of the enrollment message template Tokenize platform name in message. Changing to a datetime enrollment approach Adding sorting. A little refactoring. Adding confguration model for time delta Adding admin registration and basic form for new config model. Fixing docstring typo Updating default time delta to 0, adding test to show it disabled functionality. Removing the form from configuration and tweaking the enrollment message html --- common/djangoapps/course_modes/views.py | 2 +- common/djangoapps/student/admin.py | 5 +- common/djangoapps/student/forms.py | 3 +- .../migrations/0041_add_dashboard_config.py | 179 ++++++++++++++++++ common/djangoapps/student/models.py | 18 ++ .../student/tests/test_recent_enrollments.py | 127 +++++++++++++ common/djangoapps/student/views.py | 52 ++++- lms/templates/dashboard.html | 4 + .../enrollment/course_enrollment_message.html | 6 + 9 files changed, 391 insertions(+), 5 deletions(-) create mode 100644 common/djangoapps/student/migrations/0041_add_dashboard_config.py create mode 100644 common/djangoapps/student/tests/test_recent_enrollments.py create mode 100644 lms/templates/enrollment/course_enrollment_message.html diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index 79e12a452e6..afc63362ff2 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -157,7 +157,7 @@ class ChooseModeView(View): # it doesn't matter, but it will avoid hitting the database. if requested_mode == 'honor': CourseEnrollment.enroll(user, course_key, requested_mode) - return redirect('dashboard') + return redirect(reverse('dashboard')) mode_info = allowed_modes[requested_mode] diff --git a/common/djangoapps/student/admin.py b/common/djangoapps/student/admin.py index 40bd4742be4..9797d4bb5ef 100644 --- a/common/djangoapps/student/admin.py +++ b/common/djangoapps/student/admin.py @@ -1,8 +1,9 @@ ''' django admin pages for courseware model ''' +from config_models.admin import ConfigurationModelAdmin -from student.models import UserProfile, UserTestGroup, CourseEnrollmentAllowed +from student.models import UserProfile, UserTestGroup, CourseEnrollmentAllowed, DashboardConfiguration from student.models import CourseEnrollment, Registration, PendingNameChange, CourseAccessRole, CourseAccessRoleAdmin from ratelimitbackend import admin @@ -19,3 +20,5 @@ admin.site.register(Registration) admin.site.register(PendingNameChange) admin.site.register(CourseAccessRole, CourseAccessRoleAdmin) + +admin.site.register(DashboardConfiguration, ConfigurationModelAdmin) diff --git a/common/djangoapps/student/forms.py b/common/djangoapps/student/forms.py index fdef5da3eba..ec30aae4dea 100644 --- a/common/djangoapps/student/forms.py +++ b/common/djangoapps/student/forms.py @@ -6,6 +6,7 @@ from django.contrib.auth.models import User from django.contrib.auth.forms import PasswordResetForm from django.contrib.auth.hashers import UNUSABLE_PASSWORD + class PasswordResetFormNoActive(PasswordResetForm): def clean_email(self): """ @@ -21,4 +22,4 @@ class PasswordResetFormNoActive(PasswordResetForm): if any((user.password == UNUSABLE_PASSWORD) for user in self.users_cache): raise forms.ValidationError(self.error_messages['unusable']) - return email + return email \ No newline at end of file diff --git a/common/djangoapps/student/migrations/0041_add_dashboard_config.py b/common/djangoapps/student/migrations/0041_add_dashboard_config.py new file mode 100644 index 00000000000..a8f8af5fb51 --- /dev/null +++ b/common/djangoapps/student/migrations/0041_add_dashboard_config.py @@ -0,0 +1,179 @@ +# -*- 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 model 'DashboardConfiguration' + db.create_table('student_dashboardconfiguration', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('change_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('changed_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, on_delete=models.PROTECT)), + ('enabled', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('recent_enrollment_time_delta', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)), + )) + db.send_create_signal('student', ['DashboardConfiguration']) + + + def backwards(self, orm): + # Deleting model 'DashboardConfiguration' + db.delete_table('student_dashboardconfiguration') + + + 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'}) + }, + 'student.anonymoususerid': { + 'Meta': {'object_name': 'AnonymousUserId'}, + 'anonymous_user_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}), + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'student.courseaccessrole': { + 'Meta': {'unique_together': "(('user', 'org', 'course_id', 'role'),)", 'object_name': 'CourseAccessRole'}, + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'org': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'blank': 'True'}), + 'role': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'student.courseenrollment': { + 'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'}, + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '100'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'student.courseenrollmentallowed': { + 'Meta': {'unique_together': "(('email', 'course_id'),)", 'object_name': 'CourseEnrollmentAllowed'}, + 'auto_enroll': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'student.dashboardconfiguration': { + 'Meta': {'object_name': 'DashboardConfiguration'}, + 'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'recent_enrollment_time_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}) + }, + 'student.loginfailures': { + 'Meta': {'object_name': 'LoginFailures'}, + 'failure_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'lockout_until': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'student.passwordhistory': { + 'Meta': {'object_name': 'PasswordHistory'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'time_set': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'student.pendingemailchange': { + 'Meta': {'object_name': 'PendingEmailChange'}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.pendingnamechange': { + 'Meta': {'object_name': 'PendingNameChange'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.registration': { + 'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"}, + 'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'student.userprofile': { + 'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"}, + 'allow_certificate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'city': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'null': 'True', 'blank': 'True'}), + 'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}), + 'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), + 'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}), + 'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'student.usersignupsource': { + 'Meta': {'object_name': 'UserSignupSource'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'site': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'student.userstanding': { + 'Meta': {'object_name': 'UserStanding'}, + 'account_status': ('django.db.models.fields.CharField', [], {'max_length': '31', 'blank': 'True'}), + 'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'standing_last_changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'standing'", 'unique': 'True', 'to': "orm['auth.User']"}) + }, + 'student.usertestgroup': { + 'Meta': {'object_name': 'UserTestGroup'}, + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'}) + } + } + + complete_apps = ['student'] \ No newline at end of file diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 792382a4a30..13910c2fe2d 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -32,6 +32,7 @@ from django.dispatch import receiver, Signal from django.core.exceptions import ObjectDoesNotExist from django.utils.translation import ugettext_noop from django_countries import CountryField +from config_models.models import ConfigurationModel from track import contexts from eventtracking import tracker from importlib import import_module @@ -1388,3 +1389,20 @@ def enforce_single_login(sender, request, user, signal, **kwargs): # pylint: else: key = None user.profile.set_login_session(key) + + +class DashboardConfiguration(ConfigurationModel): + """Dashboard Configuration settings. + + Includes configuration options for the dashboard, which impact behavior and rendering for the application. + + """ + recent_enrollment_time_delta = models.PositiveIntegerField( + default=0, + help_text="The number of seconds in which a new enrollment is considered 'recent'. " + "Used to display notifications." + ) + + @property + def recent_enrollment_seconds(self): + return self.recent_enrollment_time_delta diff --git a/common/djangoapps/student/tests/test_recent_enrollments.py b/common/djangoapps/student/tests/test_recent_enrollments.py new file mode 100644 index 00000000000..f7b341361c4 --- /dev/null +++ b/common/djangoapps/student/tests/test_recent_enrollments.py @@ -0,0 +1,127 @@ +""" +Tests for the recently enrolled messaging within the Dashboard. +""" +import datetime +from django.conf import settings +from django.core.urlresolvers import reverse +from django.test import Client +from opaque_keys.edx import locator +from pytz import UTC +import unittest + +from student.tests.factories import UserFactory +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory +from student.models import CourseEnrollment, DashboardConfiguration +from student.views import get_course_enrollment_pairs, _get_recently_enrolled_courses + + +class TestRecentEnrollments(ModuleStoreTestCase): + """ + Unit tests for getting the list of courses for a logged in user + """ + def setUp(self): + """ + Add a student + """ + super(TestRecentEnrollments, self).setUp() + self.student = UserFactory() + + # Old Course + old_course_location = locator.CourseLocator('Org0', 'Course0', 'Run0') + course, enrollment = self._create_course_and_enrollment(old_course_location) + enrollment.created = datetime.datetime(1900, 12, 31, 0, 0, 0, 0) + enrollment.save() + + # New Course + course_location = locator.CourseLocator('Org1', 'Course1', 'Run1') + self._create_course_and_enrollment(course_location) + + def _create_course_and_enrollment(self, course_location): + """ Creates a course and associated enrollment. """ + course = CourseFactory.create( + org=course_location.org, + number=course_location.course, + run=course_location.run + ) + enrollment = CourseEnrollment.enroll(self.student, course.id) + return course, enrollment + + def test_recently_enrolled_courses(self): + """ + Test if the function for filtering recent enrollments works appropriately. + """ + config = DashboardConfiguration(recent_enrollment_time_delta=60) + config.save() + # get courses through iterating all courses + courses_list = list(get_course_enrollment_pairs(self.student, None, [])) + self.assertEqual(len(courses_list), 2) + + recent_course_list = _get_recently_enrolled_courses(courses_list) + self.assertEqual(len(recent_course_list), 1) + + def test_zero_second_delta(self): + """ + Tests that the recent enrollment list is empty if configured to zero seconds. + """ + config = DashboardConfiguration(recent_enrollment_time_delta=0) + config.save() + courses_list = list(get_course_enrollment_pairs(self.student, None, [])) + self.assertEqual(len(courses_list), 2) + + recent_course_list = _get_recently_enrolled_courses(courses_list) + self.assertEqual(len(recent_course_list), 0) + + def test_enrollments_sorted_most_recent(self): + """ + Test that the list of newly created courses are properly sorted to show the most + recent enrollments first. + + """ + config = DashboardConfiguration(recent_enrollment_time_delta=600) + config.save() + + # Create a number of new enrollments and courses, and force their creation behind + # the first enrollment + course_location = locator.CourseLocator('Org2', 'Course2', 'Run2') + _, enrollment2 = self._create_course_and_enrollment(course_location) + enrollment2.created = datetime.datetime.now(UTC) - datetime.timedelta(seconds=5) + enrollment2.save() + + course_location = locator.CourseLocator('Org3', 'Course3', 'Run3') + _, enrollment3 = self._create_course_and_enrollment(course_location) + enrollment3.created = datetime.datetime.now(UTC) - datetime.timedelta(seconds=10) + enrollment3.save() + + course_location = locator.CourseLocator('Org4', 'Course4', 'Run4') + _, enrollment4 = self._create_course_and_enrollment(course_location) + enrollment4.created = datetime.datetime.now(UTC) - datetime.timedelta(seconds=15) + enrollment4.save() + + course_location = locator.CourseLocator('Org5', 'Course5', 'Run5') + _, enrollment5 = self._create_course_and_enrollment(course_location) + enrollment5.created = datetime.datetime.now(UTC) - datetime.timedelta(seconds=20) + enrollment5.save() + + courses_list = list(get_course_enrollment_pairs(self.student, None, [])) + self.assertEqual(len(courses_list), 6) + + recent_course_list = _get_recently_enrolled_courses(courses_list) + self.assertEqual(len(recent_course_list), 5) + + self.assertEqual(recent_course_list[1][1], enrollment2) + self.assertEqual(recent_course_list[2][1], enrollment3) + self.assertEqual(recent_course_list[3][1], enrollment4) + self.assertEqual(recent_course_list[4][1], enrollment5) + + @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') + def test_dashboard_rendering(self): + """ + Tests that the dashboard renders the recent enrollment messages appropriately. + """ + config = DashboardConfiguration(recent_enrollment_time_delta=600) + config.save() + self.client = Client() + self.client.login(username=self.student.username, password='test') + response = self.client.get(reverse("dashboard")) + self.assertContains(response, "You have successfully enrolled in") diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 98de22096a1..2e7a31d3edf 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -46,8 +46,8 @@ from student.models import ( Registration, UserProfile, PendingNameChange, PendingEmailChange, CourseEnrollment, unique_id_for_user, CourseEnrollmentAllowed, UserStanding, LoginFailures, - create_comments_service_user, PasswordHistory, UserSignupSource -) + create_comments_service_user, PasswordHistory, UserSignupSource, + DashboardConfiguration) from student.forms import PasswordResetFormNoActive from verify_student.models import SoftwareSecurePhotoVerification, MidcourseReverificationWindow @@ -471,6 +471,10 @@ def dashboard(request): # enrollments, because it could have been a data push snafu. course_enrollment_pairs = list(get_course_enrollment_pairs(user, course_org_filter, org_filter_out_set)) + # Check to see if the student has recently enrolled in a course. If so, display a notification message confirming + # the enrollment. + enrollment_message = _create_recent_enrollment_message(course_enrollment_pairs) + course_optouts = Optout.objects.filter(user=user).values_list('course_id', flat=True) message = "" @@ -551,6 +555,7 @@ def dashboard(request): current_language = settings.LANGUAGE_DICT[settings.LANGUAGE_CODE] context = { + 'enrollment_message': enrollment_message, 'course_enrollment_pairs': course_enrollment_pairs, 'course_optouts': course_optouts, 'message': message, @@ -586,6 +591,49 @@ def dashboard(request): return render_to_response('dashboard.html', context) +def _create_recent_enrollment_message(course_enrollment_pairs): + """Builds a recent course enrollment message + + Constructs a new message template based on any recent course enrollments for the student. + + Args: + course_enrollment_pairs (list): A list of tuples containing courses, and the associated enrollment information. + + Returns: + A string representing the HTML message output from the message template. + + """ + recent_course_enrollment_pairs = _get_recently_enrolled_courses(course_enrollment_pairs) + if recent_course_enrollment_pairs: + return render_to_string( + 'enrollment/course_enrollment_message.html', + {'recent_course_enrollment_pairs': recent_course_enrollment_pairs,} + ) + + +def _get_recently_enrolled_courses(course_enrollment_pairs): + """Checks to see if the student has recently enrolled in courses. + + Checks to see if any of the enrollments in the course_enrollment_pairs have been recently created and activated. + + Args: + course_enrollment_pairs (list): A list of tuples containing courses, and the associated enrollment information. + + Returns: + A list of tuples for the course and enrollment. + + """ + seconds = DashboardConfiguration.current().recent_enrollment_time_delta + sorted_list = sorted(course_enrollment_pairs, key=lambda created: created[1].created, reverse=True) + time_delta = (datetime.datetime.now(UTC) - datetime.timedelta(seconds=seconds)) + return [ + (course, enrollment) for course, enrollment in sorted_list + # If the enrollment has no created date, we are explicitly excluding the course + # from the list of recent enrollments. + if enrollment.is_active and enrollment.created > time_delta + ] + + def try_change_enrollment(request): """ This method calls change_enrollment if the necessary POST diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index b431e79351d..230815dd265 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -192,6 +192,10 @@ </section> %endif + %if enrollment_message: + ${enrollment_message} + %endif + % if duplicate_provider: <section class="dashboard-banner third-party-auth"> ## Translators: this message is displayed when a user tries to link their account with a third-party authentication provider (for example, Google or LinkedIn) with a given edX account, but their third-party account is already associated with another edX account. provider_name is the name of the third-party authentication provider, and platform_name is the name of the edX deployment. diff --git a/lms/templates/enrollment/course_enrollment_message.html b/lms/templates/enrollment/course_enrollment_message.html new file mode 100644 index 00000000000..f2d605476f8 --- /dev/null +++ b/lms/templates/enrollment/course_enrollment_message.html @@ -0,0 +1,6 @@ +<%! from django.utils.translation import ugettext as _ %> +% for course, enrollment in recent_course_enrollment_pairs: + <section class="dashboard-banner"> + <p class='activation-message'>${_("You have successfully enrolled in {enrolled_course}.").format(enrolled_course=course.display_name)}</p> + </section> +% endfor -- GitLab