diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 256751eb6d..fc3460628f 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -36,13 +36,15 @@ from importlib import import_module from opaque_keys.edx.locations import SlashSeparatedCourseKey -from course_modes.models import CourseMode import lms.lib.comment_client as cc from util.query import use_read_replica_if_available from xmodule_django.models import CourseKeyField, NoneToEmptyManager from opaque_keys.edx.keys import CourseKey from functools import total_ordering +from certificates.models import GeneratedCertificate +from course_modes.models import CourseMode + unenroll_done = Signal(providing_args=["course_enrollment"]) log = logging.getLogger(__name__) AUDIT_LOG = logging.getLogger("audit") @@ -953,6 +955,11 @@ class CourseEnrollment(models.Model): # (side-effects are bad) if getattr(self, 'can_refund', None) is not None: return True + + # If the student has already been given a certificate they should not be refunded + if GeneratedCertificate.certificate_for_student(self.user, self.course_id) is not None: + return False + course_mode = CourseMode.mode_for_course(self.course_id, 'verified') if course_mode is None: return False diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index d9be628d94..f8838213b8 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -30,6 +30,8 @@ from student.views import (process_survey_link, _cert_info, change_enrollment, complete_course_mode_info) from student.tests.factories import UserFactory, CourseModeFactory +from certificates.models import CertificateStatuses +from certificates.tests.factories import GeneratedCertificateFactory import shoppingcart log = logging.getLogger(__name__) @@ -212,6 +214,7 @@ class DashboardTest(TestCase): self.assertFalse(course_mode_info['show_upsell']) self.assertIsNone(course_mode_info['days_for_upsell']) + @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') def test_refundable(self): verified_mode = CourseModeFactory.create( course_id=self.course.id, @@ -227,6 +230,26 @@ class DashboardTest(TestCase): verified_mode.save() self.assertFalse(enrollment.refundable()) + @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') + def test_refundable_when_certificate_exists(self): + verified_mode = CourseModeFactory.create( + course_id=self.course.id, + mode_slug='verified', + mode_display_name='Verified', + expiration_datetime=datetime.now(pytz.UTC) + timedelta(days=1) + ) + enrollment = CourseEnrollment.enroll(self.user, self.course.id, mode='verified') + + self.assertTrue(enrollment.refundable()) + + generated_certificate = GeneratedCertificateFactory.create( + user=self.user, + course_id=self.course.id, + status=CertificateStatuses.downloadable, + mode='verified' + ) + + self.assertFalse(enrollment.refundable()) class EnrollInCourseTest(TestCase): """Tests enrolling and unenrolling in courses.""" diff --git a/lms/djangoapps/certificates/models.py b/lms/djangoapps/certificates/models.py index 71ab9ffcf4..8ec9ccd05e 100644 --- a/lms/djangoapps/certificates/models.py +++ b/lms/djangoapps/certificates/models.py @@ -81,6 +81,9 @@ class CertificateWhitelist(models.Model): class GeneratedCertificate(models.Model): + + MODES = Choices('verified', 'honor', 'audit') + user = models.ForeignKey(User) course_id = CourseKeyField(max_length=255, blank=True, default=None) verify_uuid = models.CharField(max_length=32, blank=True, default='') @@ -90,7 +93,6 @@ class GeneratedCertificate(models.Model): key = models.CharField(max_length=32, blank=True, default='') distinction = models.BooleanField(default=False) status = models.CharField(max_length=32, default='unavailable') - MODES = Choices('verified', 'honor', 'audit') mode = models.CharField(max_length=32, choices=MODES, default=MODES.honor) name = models.CharField(blank=True, max_length=255) created_date = models.DateTimeField( @@ -102,6 +104,18 @@ class GeneratedCertificate(models.Model): class Meta: unique_together = (('user', 'course_id'),) + @classmethod + def certificate_for_student(cls, student, course_id): + """ + This returns the certificate for a student for a particular course + or None if no such certificate exits. + """ + try: + return cls.objects.get(user=student, course_id=course_id) + except cls.DoesNotExist: + pass + + return None def certificate_status_for_student(student, course_id): ''' diff --git a/lms/djangoapps/certificates/tests/__init__.py b/lms/djangoapps/certificates/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/certificates/tests/factories.py b/lms/djangoapps/certificates/tests/factories.py new file mode 100644 index 0000000000..3d6d92ef8c --- /dev/null +++ b/lms/djangoapps/certificates/tests/factories.py @@ -0,0 +1,16 @@ +from factory.django import DjangoModelFactory + +from opaque_keys.edx.locations import SlashSeparatedCourseKey + +from certificates.models import GeneratedCertificate, CertificateStatuses + +# Factories don't have __init__ methods, and are self documenting +# pylint: disable=W0232 +class GeneratedCertificateFactory(DjangoModelFactory): + + FACTORY_FOR = GeneratedCertificate + + course_id = None + status = CertificateStatuses.unavailable + mode = GeneratedCertificate.MODES.honor + name = '' diff --git a/lms/djangoapps/certificates/tests/tests.py b/lms/djangoapps/certificates/tests/tests.py new file mode 100644 index 0000000000..619d458912 --- /dev/null +++ b/lms/djangoapps/certificates/tests/tests.py @@ -0,0 +1,24 @@ +""" +Tests for the certificates models. +""" + +from django.test import TestCase + +from xmodule.modulestore.tests.factories import CourseFactory + +from student.tests.factories import UserFactory +from certificates.models import CertificateStatuses, GeneratedCertificate, certificate_status_for_student + + +class CertificatesModelTest(TestCase): + """ + Tests for the GeneratedCertificate model + """ + + def test_certificate_status_for_student(self): + student = UserFactory() + course = CourseFactory.create(org='edx', number='verified', display_name='Verified Course') + + certificate_status = certificate_status_for_student(student, course.id) + self.assertEqual(certificate_status['status'], CertificateStatuses.unavailable) + self.assertEqual(certificate_status['mode'], GeneratedCertificate.MODES.honor) diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 930f8304f6..abebd0fb11 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -2,6 +2,8 @@ Instructor Dashboard Views """ +import logging + from django.utils.translation import ugettext as _ from django_future.csrf import ensure_csrf_cookie from django.views.decorators.cache import cache_control @@ -27,12 +29,14 @@ from student.models import CourseEnrollment from bulk_email.models import CourseAuthorization from class_dashboard.dashboard_data import get_section_display_name, get_array_section_has_problem -from analyticsclient.client import RestClient +from analyticsclient.client import RestClient, ClientError from analyticsclient.course import Course from .tools import get_units_with_due_date, title_or_url, bulk_email_is_enabled_for_course from opaque_keys.edx.locations import SlashSeparatedCourseKey +log = logging.getLogger(__name__) + @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) @@ -250,22 +254,7 @@ def _section_analytics(course_key, access): } if settings.FEATURES.get('ENABLE_ANALYTICS_ACTIVE_COUNT'): - auth_token = settings.ANALYTICS_DATA_TOKEN - base_url = settings.ANALYTICS_DATA_URL - - client = RestClient(base_url=base_url, auth_token=auth_token) - course = Course(client, course_key) - - section_data['active_student_count'] = course.recent_active_user_count['count'] - - def format_date(value): - return value.split('T')[0] - - start = course.recent_active_user_count['interval_start'] - end = course.recent_active_user_count['interval_end'] - - section_data['active_student_count_start'] = format_date(start) - section_data['active_student_count_end'] = format_date(end) + _update_active_students(course_key, section_data) return section_data @@ -284,3 +273,30 @@ def _section_metrics(course_key, access): 'post_metrics_data_csv_url': reverse('post_metrics_data_csv'), } return section_data + + +def _update_active_students(course_key, section_data): + auth_token = settings.ANALYTICS_DATA_TOKEN + base_url = settings.ANALYTICS_DATA_URL + + section_data['active_student_count'] = 'N/A' + section_data['active_student_count_start'] = 'N/A' + section_data['active_student_count_end'] = 'N/A' + + try: + client = RestClient(base_url=base_url, auth_token=auth_token) + course = Course(client, course_key.to_deprecated_string()) + + section_data['active_student_count'] = course.recent_active_user_count['count'] + + def format_date(value): + return value.split('T')[0] + + start = course.recent_active_user_count['interval_start'] + end = course.recent_active_user_count['interval_end'] + + section_data['active_student_count_start'] = format_date(start) + section_data['active_student_count_end'] = format_date(end) + + except (ClientError, KeyError) as e: + log.exception(e) diff --git a/lms/urls.py b/lms/urls.py index cc74d0c1ad..063be31b55 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -99,7 +99,7 @@ if settings.FEATURES["ENABLE_SYSADMIN_DASHBOARD"]: ) urlpatterns += ( - url(r'support/', include('dashboard.support_urls')), + url(r'^support/', include('dashboard.support_urls')), ) #Semi-static views (these need to be rendered and have the login bar, but don't change) diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index 0293c918c9..0ffca52e33 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -27,7 +27,7 @@ -e git+https://github.com/edx/bok-choy.git@82b4e82d79b9d4c6d087ebbfa26ea23235728e62#egg=bok_choy -e git+https://github.com/edx-solutions/django-splash.git@9965a53c269666a30bb4e2b3f6037c138aef2a55#egg=django-splash -e git+https://github.com/edx/acid-block.git@459aff7b63db8f2c5decd1755706c1a64fb4ebb1#egg=acid-xblock --e git+https://github.com/edx/edx-ora2.git@release-2014-06-30T13.39#egg=edx-ora2 +-e git+https://github.com/edx/edx-ora2.git@release-2014-06-23T13.19#egg=edx-ora2 -e git+https://github.com/edx/opaque-keys.git@5929789900b3d0a354ce7274bde74edfd0430f03#egg=opaque-keys --e git+https://github.com/edx/ease.git@f9f47fb6b5c7c8b6c3360efa72eb56561e1a03b0#egg=ease +-e git+https://github.com/edx/ease.git@97de68448e5495385ba043d3091f570a699d5b5f#egg=ease -e git+https://github.com/edx/i18n-tools.git@f5303e82dff368c7595884d9325aeea1d802da25#egg=i18n-tools