diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index 79e12a452e60d51bfb068d558726c61e1210df01..afc63362ff23f7090ac1e00f7ac0e86e7a9d5201 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 40bd4742be45f8978525586456810bc73cca5735..9797d4bb5ef5066e06a62f1a9fb41df941b4f4b1 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 fdef5da3eba2cdfaa177024073aec66ace0f3836..ec30aae4dea2217e7bc8aa73329cd5b7a10a776c 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 0000000000000000000000000000000000000000..a8f8af5fb51ce237e5b09900d2976208471564b1 --- /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 792382a4a305c86385b02eb73ff8e3a03f7e4ab8..13910c2fe2d757694bf229f468d3c0d0649b03bb 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 0000000000000000000000000000000000000000..f7b341361c4fcb919f6bd4abc19976df4c87c221 --- /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 98de22096a1a54a4480b11e6bec08d36f47ee0ed..2e7a31d3edfde778cfe4b80335a4470d36decbbc 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 b431e79351deba5a7da8caa578513c655bfb430b..230815dd2658c3e58363a8fe2cae456bb231cf79 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 0000000000000000000000000000000000000000..f2d605476f8304cd81a84a7f6d61f008750e3bc1 --- /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