Commit b9c15dab by Peter Fogg

Merge pull request #11785 from edx/peter-fogg/audit-certs-dogwood

Port audit cert changes to Dogwood.
parents fdeceb10 9ee0162d
...@@ -159,7 +159,9 @@ class CourseMode(models.Model): ...@@ -159,7 +159,9 @@ class CourseMode(models.Model):
@expiration_datetime.setter @expiration_datetime.setter
def expiration_datetime(self, new_datetime): def expiration_datetime(self, new_datetime):
""" Saves datetime to _expiration_datetime and sets the explicit flag. """ """ Saves datetime to _expiration_datetime and sets the explicit flag. """
self.expiration_datetime_is_explicit = True # Only set explicit flag if we are setting an actual date.
if new_datetime is not None:
self.expiration_datetime_is_explicit = True
self._expiration_datetime = new_datetime self._expiration_datetime = new_datetime
@classmethod @classmethod
...@@ -590,6 +592,18 @@ class CourseMode(models.Model): ...@@ -590,6 +592,18 @@ class CourseMode(models.Model):
modes = cls.modes_for_course(course_id) modes = cls.modes_for_course(course_id)
return min(mode.min_price for mode in modes if mode.currency.lower() == currency.lower()) return min(mode.min_price for mode in modes if mode.currency.lower() == currency.lower())
@classmethod
def is_eligible_for_certificate(cls, mode_slug):
"""
Returns whether or not the given mode_slug is eligible for a
certificate. Currently all modes other than 'audit' grant a
certificate. Note that audit enrollments which existed prior
to December 2015 *were* given certificates, so there will be
GeneratedCertificate records with mode='audit' which are
eligible.
"""
return mode_slug != cls.AUDIT
def to_tuple(self): def to_tuple(self):
""" """
Takes a mode model and turns it into a model named tuple. Takes a mode model and turns it into a model named tuple.
......
...@@ -421,3 +421,25 @@ class CourseModeModelTest(TestCase): ...@@ -421,3 +421,25 @@ class CourseModeModelTest(TestCase):
self.assertFalse(verified_mode.expiration_datetime_is_explicit) self.assertFalse(verified_mode.expiration_datetime_is_explicit)
self.assertEqual(verified_mode.expiration_datetime, now) self.assertEqual(verified_mode.expiration_datetime, now)
def test_expiration_datetime_explicitly_set_to_none(self):
""" Verify that setting the _expiration_date property does not set the explicit flag. """
verified_mode, __ = self.create_mode('verified', 'Verified Certificate')
self.assertFalse(verified_mode.expiration_datetime_is_explicit)
verified_mode.expiration_datetime = None
self.assertFalse(verified_mode.expiration_datetime_is_explicit)
self.assertIsNone(verified_mode.expiration_datetime)
@ddt.data(
(CourseMode.AUDIT, False),
(CourseMode.HONOR, True),
(CourseMode.VERIFIED, True),
(CourseMode.CREDIT_MODE, True),
(CourseMode.PROFESSIONAL, True),
(CourseMode.NO_ID_PROFESSIONAL_MODE, True),
)
@ddt.unpack
def test_eligible_for_cert(self, mode_slug, expected_eligibility):
"""Verify that non-audit modes are eligible for a cert."""
self.assertEqual(CourseMode.is_eligible_for_certificate(mode_slug), expected_eligibility)
...@@ -97,7 +97,9 @@ class Command(BaseCommand): ...@@ -97,7 +97,9 @@ class Command(BaseCommand):
cert_grades = { cert_grades = {
cert.user.username: cert.grade cert.user.username: cert.grade
for cert in list( for cert in list(
GeneratedCertificate.objects.filter(course_id=course_key).prefetch_related('user') GeneratedCertificate.objects.filter( # pylint: disable=no-member
course_id=course_key
).prefetch_related('user')
) )
} }
print "Grading students" print "Grading students"
......
...@@ -296,6 +296,8 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa ...@@ -296,6 +296,8 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa
CertificateStatuses.notpassing: 'notpassing', CertificateStatuses.notpassing: 'notpassing',
CertificateStatuses.restricted: 'restricted', CertificateStatuses.restricted: 'restricted',
CertificateStatuses.auditing: 'auditing', CertificateStatuses.auditing: 'auditing',
CertificateStatuses.audit_passing: 'auditing',
CertificateStatuses.audit_notpassing: 'auditing',
} }
default_status = 'processing' default_status = 'processing'
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
LOCK TABLES `django_migrations` WRITE; LOCK TABLES `django_migrations` WRITE;
/*!40000 ALTER TABLE `django_migrations` DISABLE KEYS */; /*!40000 ALTER TABLE `django_migrations` DISABLE KEYS */;
INSERT INTO `django_migrations` VALUES (1,'contenttypes','0001_initial','2015-11-18 15:37:03.679215'),(2,'auth','0001_initial','2015-11-18 15:37:04.239453'),(3,'admin','0001_initial','2015-11-18 15:37:04.375156'),(4,'assessment','0001_initial','2015-11-18 15:37:09.037208'),(5,'contenttypes','0002_remove_content_type_name','2015-11-18 15:37:09.345424'),(6,'auth','0002_alter_permission_name_max_length','2015-11-18 15:37:09.479742'),(7,'auth','0003_alter_user_email_max_length','2015-11-18 15:37:09.677049'),(8,'auth','0004_alter_user_username_opts','2015-11-18 15:37:09.805606'),(9,'auth','0005_alter_user_last_login_null','2015-11-18 15:37:09.949394'),(10,'auth','0006_require_contenttypes_0002','2015-11-18 15:37:09.978283'),(11,'branding','0001_initial','2015-11-18 15:37:10.295010'),(12,'bulk_email','0001_initial','2015-11-18 15:37:10.849873'),(13,'bulk_email','0002_data__load_course_email_template','2015-11-18 15:37:10.939652'),(14,'certificates','0001_initial','2015-11-18 15:37:12.698538'),(15,'certificates','0002_data__certificatehtmlviewconfiguration_data','2015-11-18 15:37:12.746221'),(16,'certificates','0003_data__default_modes','2015-11-18 15:37:13.182516'),(17,'commerce','0001_data__add_ecommerce_service_user','2015-11-18 15:37:13.269990'),(18,'cors_csrf','0001_initial','2015-11-18 15:37:13.459604'),(19,'course_action_state','0001_initial','2015-11-18 15:37:14.011267'),(20,'course_groups','0001_initial','2015-11-18 15:37:15.487063'),(21,'course_modes','0001_initial','2015-11-18 15:37:15.719347'),(22,'course_overviews','0001_initial','2015-11-18 15:37:15.932886'),(23,'course_structures','0001_initial','2015-11-18 15:37:16.000954'),(24,'courseware','0001_initial','2015-11-18 15:37:18.771429'),(25,'credit','0001_initial','2015-11-18 15:37:21.519361'),(26,'dark_lang','0001_initial','2015-11-18 15:37:21.846133'),(27,'dark_lang','0002_data__enable_on_install','2015-11-18 15:37:21.995633'),(28,'default','0001_initial','2015-11-18 15:37:23.147337'),(29,'default','0002_add_related_name','2015-11-18 15:37:23.421429'),(30,'default','0003_alter_email_max_length','2015-11-18 15:37:23.512312'),(31,'django_comment_common','0001_initial','2015-11-18 15:37:24.320730'),(32,'django_notify','0001_initial','2015-11-18 15:37:25.685195'),(33,'django_openid_auth','0001_initial','2015-11-18 15:37:26.276837'),(34,'edx_proctoring','0001_initial','2015-11-18 15:37:30.965630'),(35,'edxval','0001_initial','2015-11-18 15:37:32.899299'),(36,'edxval','0002_data__default_profiles','2015-11-18 15:37:32.962490'),(37,'embargo','0001_initial','2015-11-18 15:37:34.781531'),(38,'embargo','0002_data__add_countries','2015-11-18 15:37:36.006428'),(39,'external_auth','0001_initial','2015-11-18 15:37:36.857447'),(40,'foldit','0001_initial','2015-11-18 15:37:38.106002'),(41,'instructor_task','0001_initial','2015-11-18 15:37:38.670378'),(42,'licenses','0001_initial','2015-11-18 15:37:39.225872'),(43,'lms_xblock','0001_initial','2015-11-18 15:37:39.674372'),(44,'milestones','0001_initial','2015-11-18 15:37:41.240336'),(45,'milestones','0002_data__seed_relationship_types','2015-11-18 15:37:41.287383'),(46,'mobile_api','0001_initial','2015-11-18 15:37:41.822343'),(47,'notes','0001_initial','2015-11-18 15:37:42.381389'),(48,'oauth2','0001_initial','2015-11-18 15:37:45.638217'),(49,'oauth2_provider','0001_initial','2015-11-18 15:37:46.173938'),(50,'oauth_provider','0001_initial','2015-11-18 15:37:47.398936'),(51,'organizations','0001_initial','2015-11-18 15:37:47.863329'),(52,'programs','0001_initial','2015-11-18 15:37:48.427080'),(53,'psychometrics','0001_initial','2015-11-18 15:37:48.987109'),(54,'self_paced','0001_initial','2015-11-18 15:37:49.523078'),(55,'sessions','0001_initial','2015-11-18 15:37:49.646106'),(56,'student','0001_initial','2015-11-18 15:38:05.543390'),(57,'shoppingcart','0001_initial','2015-11-18 15:38:21.727512'),(58,'sites','0001_initial','2015-11-18 15:38:21.837391'),(59,'splash','0001_initial','2015-11-18 15:38:23.244678'),(60,'status','0001_initial','2015-11-18 15:38:24.981737'),(61,'submissions','0001_initial','2015-11-18 15:38:26.499689'),(62,'survey','0001_initial','2015-11-18 15:38:28.962457'),(63,'teams','0001_initial','2015-11-18 15:38:31.641489'),(64,'third_party_auth','0001_initial','2015-11-18 15:38:35.861826'),(65,'track','0001_initial','2015-11-18 15:38:35.952939'),(66,'user_api','0001_initial','2015-11-18 15:38:42.846323'),(67,'util','0001_initial','2015-11-18 15:38:43.780691'),(68,'util','0002_data__default_rate_limit_config','2015-11-18 15:38:43.844470'),(69,'verify_student','0001_initial','2015-11-18 15:39:02.098134'),(70,'wiki','0001_initial','2015-11-18 15:39:50.801336'),(71,'workflow','0001_initial','2015-11-18 15:39:51.336698'),(72,'xblock_django','0001_initial','2015-11-18 15:39:52.487567'),(73,'contentstore','0001_initial','2015-11-18 15:40:36.154872'),(74,'course_creators','0001_initial','2015-11-18 15:40:36.715711'),(75,'xblock_config','0001_initial','2015-11-18 15:40:38.167185'); INSERT INTO `django_migrations` VALUES (1,'contenttypes','0001_initial','2016-03-10 21:02:18.260628'),(2,'auth','0001_initial','2016-03-10 21:02:18.465022'),(3,'admin','0001_initial','2016-03-10 21:02:18.528057'),(4,'assessment','0001_initial','2016-03-10 21:02:21.076024'),(5,'contenttypes','0002_remove_content_type_name','2016-03-10 21:02:21.214125'),(6,'auth','0002_alter_permission_name_max_length','2016-03-10 21:02:21.277907'),(7,'auth','0003_alter_user_email_max_length','2016-03-10 21:02:21.317004'),(8,'auth','0004_alter_user_username_opts','2016-03-10 21:02:21.348775'),(9,'auth','0005_alter_user_last_login_null','2016-03-10 21:02:21.422065'),(10,'auth','0006_require_contenttypes_0002','2016-03-10 21:02:21.433636'),(11,'branding','0001_initial','2016-03-10 21:02:21.522006'),(12,'bulk_email','0001_initial','2016-03-10 21:02:21.762778'),(13,'bulk_email','0002_data__load_course_email_template','2016-03-10 21:02:21.809519'),(14,'instructor_task','0001_initial','2016-03-10 21:02:21.958763'),(15,'certificates','0001_initial','2016-03-10 21:02:22.665674'),(16,'certificates','0002_data__certificatehtmlviewconfiguration_data','2016-03-10 21:02:22.679954'),(17,'certificates','0003_data__default_modes','2016-03-10 21:02:22.738210'),(18,'certificates','0004_certificategenerationhistory','2016-03-10 21:02:22.869691'),(19,'certificates','0005_auto_20151208_0801','2016-03-10 21:02:22.939965'),(20,'commerce','0001_data__add_ecommerce_service_user','2016-03-10 21:02:22.957423'),(21,'cors_csrf','0001_initial','2016-03-10 21:02:23.030499'),(22,'course_action_state','0001_initial','2016-03-10 21:02:23.237008'),(23,'course_groups','0001_initial','2016-03-10 21:02:23.965465'),(24,'course_modes','0001_initial','2016-03-10 21:02:24.078559'),(25,'course_modes','0002_coursemode_expiration_datetime_is_explicit','2016-03-10 21:02:24.122497'),(26,'course_modes','0003_auto_20151113_1443','2016-03-10 21:02:24.143334'),(27,'course_modes','0004_auto_20151113_1457','2016-03-10 21:02:24.230123'),(28,'course_overviews','0001_initial','2016-03-10 21:02:24.291307'),(29,'course_overviews','0002_add_course_catalog_fields','2016-03-10 21:02:25.188256'),(30,'course_overviews','0003_courseoverviewgeneratedhistory','2016-03-10 21:02:25.212392'),(31,'course_overviews','0004_courseoverview_org','2016-03-10 21:02:25.256998'),(32,'course_overviews','0005_delete_courseoverviewgeneratedhistory','2016-03-10 21:02:25.277816'),(33,'course_structures','0001_initial','2016-03-10 21:02:25.303490'),(34,'courseware','0001_initial','2016-03-10 21:02:26.875590'),(35,'credit','0001_initial','2016-03-10 21:02:28.055671'),(36,'dark_lang','0001_initial','2016-03-10 21:02:28.191039'),(37,'dark_lang','0002_data__enable_on_install','2016-03-10 21:02:28.210267'),(38,'default','0001_initial','2016-03-10 21:02:28.623904'),(39,'default','0002_add_related_name','2016-03-10 21:02:28.757794'),(40,'default','0003_alter_email_max_length','2016-03-10 21:02:28.792001'),(41,'django_comment_common','0001_initial','2016-03-10 21:02:29.195893'),(42,'django_notify','0001_initial','2016-03-10 21:02:29.849286'),(43,'django_openid_auth','0001_initial','2016-03-10 21:02:30.058904'),(44,'edx_proctoring','0001_initial','2016-03-10 21:02:33.416077'),(45,'edxval','0001_initial','2016-03-10 21:02:34.027690'),(46,'edxval','0002_data__default_profiles','2016-03-10 21:02:34.061771'),(47,'embargo','0001_initial','2016-03-10 21:02:34.831364'),(48,'embargo','0002_data__add_countries','2016-03-10 21:02:35.149918'),(49,'external_auth','0001_initial','2016-03-10 21:02:35.660731'),(50,'lms_xblock','0001_initial','2016-03-10 21:02:35.887522'),(51,'milestones','0001_initial','2016-03-10 21:02:36.772432'),(52,'milestones','0002_data__seed_relationship_types','2016-03-10 21:02:36.799599'),(53,'mobile_api','0001_initial','2016-03-10 21:02:37.040219'),(54,'notes','0001_initial','2016-03-10 21:02:37.349625'),(55,'oauth2','0001_initial','2016-03-10 21:02:39.331002'),(56,'oauth2_provider','0001_initial','2016-03-10 21:02:39.564057'),(57,'oauth_provider','0001_initial','2016-03-10 21:02:40.197386'),(58,'organizations','0001_initial','2016-03-10 21:02:40.375246'),(59,'programs','0001_initial','2016-03-10 21:02:40.652142'),(60,'programs','0002_programsapiconfig_cache_ttl','2016-03-10 21:02:40.944996'),(61,'programs','0003_auto_20151120_1613','2016-03-10 21:02:42.079199'),(62,'self_paced','0001_initial','2016-03-10 21:02:42.395244'),(63,'sessions','0001_initial','2016-03-10 21:02:42.446632'),(64,'student','0001_initial','2016-03-10 21:02:53.093025'),(65,'shoppingcart','0001_initial','2016-03-10 21:03:02.773016'),(66,'shoppingcart','0002_auto_20151208_1034','2016-03-10 21:03:03.677397'),(67,'sites','0001_initial','2016-03-10 21:03:03.722504'),(68,'splash','0001_initial','2016-03-10 21:03:04.209403'),(69,'status','0001_initial','2016-03-10 21:03:05.290396'),(70,'student','0002_auto_20151208_1034','2016-03-10 21:03:06.298673'),(71,'submissions','0001_initial','2016-03-10 21:03:07.726421'),(72,'submissions','0002_auto_20151119_0913','2016-03-10 21:03:07.878500'),(73,'survey','0001_initial','2016-03-10 21:03:08.526264'),(74,'teams','0001_initial','2016-03-10 21:03:09.854204'),(75,'third_party_auth','0001_initial','2016-03-10 21:03:12.568462'),(76,'track','0001_initial','2016-03-10 21:03:12.626876'),(77,'user_api','0001_initial','2016-03-10 21:03:16.666873'),(78,'util','0001_initial','2016-03-10 21:03:17.215318'),(79,'util','0002_data__default_rate_limit_config','2016-03-10 21:03:17.256591'),(80,'verify_student','0001_initial','2016-03-10 21:03:24.649641'),(81,'verify_student','0002_auto_20151124_1024','2016-03-10 21:03:25.178614'),(82,'verify_student','0003_auto_20151113_1443','2016-03-10 21:03:25.721503'),(83,'wiki','0001_initial','2016-03-10 21:03:45.059550'),(84,'wiki','0002_remove_article_subscription','2016-03-10 21:03:45.102613'),(85,'workflow','0001_initial','2016-03-10 21:03:45.344575'),(86,'xblock_django','0001_initial','2016-03-10 21:03:46.046781'),(87,'contentstore','0001_initial','2016-03-10 21:04:02.888358'),(88,'course_creators','0001_initial','2016-03-10 21:04:02.943542'),(89,'xblock_config','0001_initial','2016-03-10 21:04:03.166075');
/*!40000 ALTER TABLE `django_migrations` ENABLE KEYS */; /*!40000 ALTER TABLE `django_migrations` ENABLE KEYS */;
UNLOCK TABLES; UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
...@@ -34,4 +34,4 @@ UNLOCK TABLES; ...@@ -34,4 +34,4 @@ UNLOCK TABLES;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2015-11-18 15:40:53 -- Dump completed on 2016-03-10 21:04:06
...@@ -386,7 +386,7 @@ CREATE TABLE `auth_permission` ( ...@@ -386,7 +386,7 @@ CREATE TABLE `auth_permission` (
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `content_type_id` (`content_type_id`,`codename`), UNIQUE KEY `content_type_id` (`content_type_id`,`codename`),
CONSTRAINT `auth__content_type_id_508cf46651277a81_fk_django_content_type_id` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`) CONSTRAINT `auth__content_type_id_508cf46651277a81_fk_django_content_type_id` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=704 DEFAULT CHARSET=utf8; ) ENGINE=InnoDB AUTO_INCREMENT=698 DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `auth_registration`; DROP TABLE IF EXISTS `auth_registration`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
...@@ -650,6 +650,24 @@ CREATE TABLE `certificates_certificategenerationcoursesetting` ( ...@@ -650,6 +650,24 @@ CREATE TABLE `certificates_certificategenerationcoursesetting` (
KEY `certificates_certificategenerationcoursesetting_c8235886` (`course_key`) KEY `certificates_certificategenerationcoursesetting_c8235886` (`course_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `certificates_certificategenerationhistory`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `certificates_certificategenerationhistory` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`created` datetime(6) NOT NULL,
`modified` datetime(6) NOT NULL,
`course_id` varchar(255) NOT NULL,
`is_regeneration` tinyint(1) NOT NULL,
`generated_by_id` int(11) NOT NULL,
`instructor_task_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `certificates_ce_generated_by_id_4679598e2d7d6e10_fk_auth_user_id` (`generated_by_id`),
KEY `D794923145b81064c232a4d0bfe79880` (`instructor_task_id`),
CONSTRAINT `D794923145b81064c232a4d0bfe79880` FOREIGN KEY (`instructor_task_id`) REFERENCES `instructor_task_instructortask` (`id`),
CONSTRAINT `certificates_ce_generated_by_id_4679598e2d7d6e10_fk_auth_user_id` FOREIGN KEY (`generated_by_id`) REFERENCES `auth_user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `certificates_certificatehtmlviewconfiguration`; DROP TABLE IF EXISTS `certificates_certificatehtmlviewconfiguration`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8 */;
...@@ -767,6 +785,7 @@ CREATE TABLE `certificates_generatedcertificate` ( ...@@ -767,6 +785,7 @@ CREATE TABLE `certificates_generatedcertificate` (
`user_id` int(11) NOT NULL, `user_id` int(11) NOT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `certificates_generatedcertificate_user_id_552a0fa6f7d3f7e8_uniq` (`user_id`,`course_id`), UNIQUE KEY `certificates_generatedcertificate_user_id_552a0fa6f7d3f7e8_uniq` (`user_id`,`course_id`),
KEY `certificates_generatedcertific_verify_uuid_1b5a14bb83c471ff_uniq` (`verify_uuid`),
CONSTRAINT `certificates_generatedc_user_id_77ed5f7a53121815_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`) CONSTRAINT `certificates_generatedc_user_id_77ed5f7a53121815_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
...@@ -959,11 +978,26 @@ CREATE TABLE `course_modes_coursemode` ( ...@@ -959,11 +978,26 @@ CREATE TABLE `course_modes_coursemode` (
`suggested_prices` varchar(255) NOT NULL, `suggested_prices` varchar(255) NOT NULL,
`description` longtext, `description` longtext,
`sku` varchar(255) DEFAULT NULL, `sku` varchar(255) DEFAULT NULL,
`expiration_datetime_is_explicit` tinyint(1) NOT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `course_modes_coursemode_course_id_6fbb1796ace558b4_uniq` (`course_id`,`mode_slug`,`currency`), UNIQUE KEY `course_modes_coursemode_course_id_6fbb1796ace558b4_uniq` (`course_id`,`mode_slug`,`currency`),
KEY `course_modes_coursemode_ea134da7` (`course_id`) KEY `course_modes_coursemode_ea134da7` (`course_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `course_modes_coursemodeexpirationconfig`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `course_modes_coursemodeexpirationconfig` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`change_date` datetime(6) NOT NULL,
`enabled` tinyint(1) NOT NULL,
`verification_window` bigint(20) NOT NULL,
`changed_by_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `course_modes_cour_changed_by_id_4d31fab2bbe98b89_fk_auth_user_id` (`changed_by_id`),
CONSTRAINT `course_modes_cour_changed_by_id_4d31fab2bbe98b89_fk_auth_user_id` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `course_modes_coursemodesarchive`; DROP TABLE IF EXISTS `course_modes_coursemodesarchive`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8 */;
...@@ -1016,6 +1050,12 @@ CREATE TABLE `course_overviews_courseoverview` ( ...@@ -1016,6 +1050,12 @@ CREATE TABLE `course_overviews_courseoverview` (
`enrollment_domain` longtext, `enrollment_domain` longtext,
`invitation_only` tinyint(1) NOT NULL, `invitation_only` tinyint(1) NOT NULL,
`max_student_enrollments_allowed` int(11) DEFAULT NULL, `max_student_enrollments_allowed` int(11) DEFAULT NULL,
`announcement` datetime(6),
`catalog_visibility` longtext,
`course_video_url` longtext,
`effort` longtext,
`short_description` longtext,
`org` longtext NOT NULL,
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
...@@ -1445,7 +1485,7 @@ CREATE TABLE `django_content_type` ( ...@@ -1445,7 +1485,7 @@ CREATE TABLE `django_content_type` (
`model` varchar(100) NOT NULL, `model` varchar(100) NOT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `django_content_type_app_label_45f3b1d93ec8c61c_uniq` (`app_label`,`model`) UNIQUE KEY `django_content_type_app_label_45f3b1d93ec8c61c_uniq` (`app_label`,`model`)
) ENGINE=InnoDB AUTO_INCREMENT=234 DEFAULT CHARSET=utf8; ) ENGINE=InnoDB AUTO_INCREMENT=232 DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `django_migrations`; DROP TABLE IF EXISTS `django_migrations`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
...@@ -1456,7 +1496,7 @@ CREATE TABLE `django_migrations` ( ...@@ -1456,7 +1496,7 @@ CREATE TABLE `django_migrations` (
`name` varchar(255) NOT NULL, `name` varchar(255) NOT NULL,
`applied` datetime(6) NOT NULL, `applied` datetime(6) NOT NULL,
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=76 DEFAULT CHARSET=utf8; ) ENGINE=InnoDB AUTO_INCREMENT=90 DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `django_openid_auth_association`; DROP TABLE IF EXISTS `django_openid_auth_association`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
...@@ -2455,8 +2495,12 @@ CREATE TABLE `programs_programsapiconfig` ( ...@@ -2455,8 +2495,12 @@ CREATE TABLE `programs_programsapiconfig` (
`internal_service_url` varchar(200) NOT NULL, `internal_service_url` varchar(200) NOT NULL,
`public_service_url` varchar(200) NOT NULL, `public_service_url` varchar(200) NOT NULL,
`api_version_number` int(11) NOT NULL, `api_version_number` int(11) NOT NULL,
`enable_student_dashboard` tinyint(1) DEFAULT NULL, `enable_student_dashboard` tinyint(1) NOT NULL,
`changed_by_id` int(11) DEFAULT NULL, `changed_by_id` int(11) DEFAULT NULL,
`cache_ttl` int(10) unsigned NOT NULL,
`authoring_app_css_path` varchar(255) NOT NULL,
`authoring_app_js_path` varchar(255) NOT NULL,
`enable_studio_tab` tinyint(1) NOT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `programs_programsa_changed_by_id_b7c3b49d5c0dcd3_fk_auth_user_id` (`changed_by_id`), KEY `programs_programsa_changed_by_id_b7c3b49d5c0dcd3_fk_auth_user_id` (`changed_by_id`),
CONSTRAINT `programs_programsa_changed_by_id_b7c3b49d5c0dcd3_fk_auth_user_id` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`) CONSTRAINT `programs_programsa_changed_by_id_b7c3b49d5c0dcd3_fk_auth_user_id` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
...@@ -3229,6 +3273,22 @@ CREATE TABLE `submissions_score` ( ...@@ -3229,6 +3273,22 @@ CREATE TABLE `submissions_score` (
CONSTRAINT `subm_submission_id_3fc975fe88442ff7_fk_submissions_submission_id` FOREIGN KEY (`submission_id`) REFERENCES `submissions_submission` (`id`) CONSTRAINT `subm_submission_id_3fc975fe88442ff7_fk_submissions_submission_id` FOREIGN KEY (`submission_id`) REFERENCES `submissions_submission` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `submissions_scoreannotation`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `submissions_scoreannotation` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`annotation_type` varchar(255) NOT NULL,
`creator` varchar(255) NOT NULL,
`reason` longtext NOT NULL,
`score_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `submissions_sc_score_id_7b5ef248552cb857_fk_submissions_score_id` (`score_id`),
KEY `submissions_scoreannotation_fd685234` (`annotation_type`),
KEY `submissions_scoreannotation_ee243325` (`creator`),
CONSTRAINT `submissions_sc_score_id_7b5ef248552cb857_fk_submissions_score_id` FOREIGN KEY (`score_id`) REFERENCES `submissions_score` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `submissions_scoresummary`; DROP TABLE IF EXISTS `submissions_scoresummary`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8 */;
...@@ -3580,6 +3640,7 @@ CREATE TABLE `verify_student_historicalverificationdeadline` ( ...@@ -3580,6 +3640,7 @@ CREATE TABLE `verify_student_historicalverificationdeadline` (
`history_date` datetime(6) NOT NULL, `history_date` datetime(6) NOT NULL,
`history_type` varchar(1) NOT NULL, `history_type` varchar(1) NOT NULL,
`history_user_id` int(11) DEFAULT NULL, `history_user_id` int(11) DEFAULT NULL,
`deadline_is_explicit` tinyint(1) NOT NULL,
PRIMARY KEY (`history_id`), PRIMARY KEY (`history_id`),
KEY `verify_student__history_user_id_1e374d24cb7902c2_fk_auth_user_id` (`history_user_id`), KEY `verify_student__history_user_id_1e374d24cb7902c2_fk_auth_user_id` (`history_user_id`),
KEY `verify_student_historicalverificationdeadline_b80bb774` (`id`), KEY `verify_student_historicalverificationdeadline_b80bb774` (`id`),
...@@ -3702,6 +3763,7 @@ CREATE TABLE `verify_student_verificationdeadline` ( ...@@ -3702,6 +3763,7 @@ CREATE TABLE `verify_student_verificationdeadline` (
`modified` datetime(6) NOT NULL, `modified` datetime(6) NOT NULL,
`course_key` varchar(255) NOT NULL, `course_key` varchar(255) NOT NULL,
`deadline` datetime(6) NOT NULL, `deadline` datetime(6) NOT NULL,
`deadline_is_explicit` tinyint(1) NOT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `course_key` (`course_key`) UNIQUE KEY `course_key` (`course_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
...@@ -3804,18 +3866,6 @@ CREATE TABLE `wiki_articlerevision` ( ...@@ -3804,18 +3866,6 @@ CREATE TABLE `wiki_articlerevision` (
CONSTRAINT `wiki_articlerevision_user_id_183520686b6ead55_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`) CONSTRAINT `wiki_articlerevision_user_id_183520686b6ead55_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `wiki_articlesubscription`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `wiki_articlesubscription` (
`articleplugin_ptr_id` int(11) NOT NULL,
`subscription_ptr_id` int(11) NOT NULL,
PRIMARY KEY (`articleplugin_ptr_id`),
UNIQUE KEY `subscription_ptr_id` (`subscription_ptr_id`),
CONSTRAINT `D3cd26aee5a69a796bee9c6aeab7e317` FOREIGN KEY (`subscription_ptr_id`) REFERENCES `notify_subscription` (`subscription_id`),
CONSTRAINT `w_articleplugin_ptr_id_489742a9a302c93d_fk_wiki_articleplugin_id` FOREIGN KEY (`articleplugin_ptr_id`) REFERENCES `wiki_articleplugin` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `wiki_attachment`; DROP TABLE IF EXISTS `wiki_attachment`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8 */;
......
...@@ -77,7 +77,7 @@ def get_certificates_for_user(username): ...@@ -77,7 +77,7 @@ def get_certificates_for_user(username):
else None else None
), ),
} }
for cert in GeneratedCertificate.objects.filter(user__username=username).order_by("course_id") for cert in GeneratedCertificate.eligible_certificates.filter(user__username=username).order_by("course_id")
] ]
...@@ -108,11 +108,14 @@ def generate_user_certificates(student, course_key, course=None, insecure=False, ...@@ -108,11 +108,14 @@ def generate_user_certificates(student, course_key, course=None, insecure=False,
if insecure: if insecure:
xqueue.use_https = False xqueue.use_https = False
generate_pdf = not has_html_certificates_enabled(course_key, course) generate_pdf = not has_html_certificates_enabled(course_key, course)
status, cert = xqueue.add_cert(student, course_key, cert = xqueue.add_cert(
course=course, student,
generate_pdf=generate_pdf, course_key,
forced_grade=forced_grade) course=course,
if status in [CertificateStatuses.generating, CertificateStatuses.downloadable]: generate_pdf=generate_pdf,
forced_grade=forced_grade
)
if cert.status in [CertificateStatuses.generating, CertificateStatuses.downloadable]:
emit_certificate_event('created', student, course_key, course, { emit_certificate_event('created', student, course_key, course, {
'user_id': student.id, 'user_id': student.id,
'course_id': unicode(course_key), 'course_id': unicode(course_key),
...@@ -120,7 +123,7 @@ def generate_user_certificates(student, course_key, course=None, insecure=False, ...@@ -120,7 +123,7 @@ def generate_user_certificates(student, course_key, course=None, insecure=False,
'enrollment_mode': cert.mode, 'enrollment_mode': cert.mode,
'generation_mode': generation_mode 'generation_mode': generation_mode
}) })
return status return cert.status
def regenerate_user_certificates(student, course_key, course=None, def regenerate_user_certificates(student, course_key, course=None,
...@@ -384,7 +387,7 @@ def get_certificate_url(user_id=None, course_id=None, uuid=None): ...@@ -384,7 +387,7 @@ def get_certificate_url(user_id=None, course_id=None, uuid=None):
) )
return url return url
try: try:
user_certificate = GeneratedCertificate.objects.get( user_certificate = GeneratedCertificate.eligible_certificates.get(
user=user_id, user=user_id,
course_id=course_id course_id=course_id
) )
......
...@@ -76,7 +76,7 @@ class Command(BaseCommand): ...@@ -76,7 +76,7 @@ class Command(BaseCommand):
status = options.get('status', CertificateStatuses.downloadable) status = options.get('status', CertificateStatuses.downloadable)
grade = options.get('grade', '') grade = options.get('grade', '')
cert, created = GeneratedCertificate.objects.get_or_create( cert, created = GeneratedCertificate.eligible_certificates.get_or_create(
user=user, user=user,
course_id=course_key course_id=course_key
) )
......
...@@ -42,8 +42,9 @@ class Command(BaseCommand): ...@@ -42,8 +42,9 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
course_id = options['course'] course_id = options['course']
print "Fetching ungraded students for {0}".format(course_id) print "Fetching ungraded students for {0}".format(course_id)
ungraded = GeneratedCertificate.objects.filter( ungraded = GeneratedCertificate.objects.filter( # pylint: disable=no-member
course_id__exact=course_id).filter(grade__exact='') course_id__exact=course_id
).filter(grade__exact='')
course = courses.get_course_by_id(course_id) course = courses.get_course_by_id(course_id)
factory = RequestFactory() factory = RequestFactory()
request = factory.get('/') request = factory.get('/')
......
...@@ -70,14 +70,17 @@ class Command(BaseCommand): ...@@ -70,14 +70,17 @@ class Command(BaseCommand):
enrolled_total = User.objects.filter( enrolled_total = User.objects.filter(
courseenrollment__course_id=course_id courseenrollment__course_id=course_id
) )
verified_enrolled = GeneratedCertificate.objects.filter( verified_enrolled = GeneratedCertificate.objects.filter( # pylint: disable=no-member
course_id__exact=course_id, mode__exact='verified' course_id__exact=course_id,
mode__exact='verified'
) )
honor_enrolled = GeneratedCertificate.objects.filter( honor_enrolled = GeneratedCertificate.objects.filter( # pylint: disable=no-member
course_id__exact=course_id, mode__exact='honor' course_id__exact=course_id,
mode__exact='honor'
) )
audit_enrolled = GeneratedCertificate.objects.filter( audit_enrolled = GeneratedCertificate.objects.filter( # pylint: disable=no-member
course_id__exact=course_id, mode__exact='audit' course_id__exact=course_id,
mode__exact='audit'
) )
cert_data[course_id] = { cert_data[course_id] = {
...@@ -88,7 +91,7 @@ class Command(BaseCommand): ...@@ -88,7 +91,7 @@ class Command(BaseCommand):
'audit_enrolled': audit_enrolled.count() 'audit_enrolled': audit_enrolled.count()
} }
status_tally = GeneratedCertificate.objects.filter( status_tally = GeneratedCertificate.objects.filter( # pylint: disable=no-member
course_id__exact=course_id course_id__exact=course_id
).values('status').annotate( ).values('status').annotate(
dcount=Count('status') dcount=Count('status')
...@@ -100,7 +103,7 @@ class Command(BaseCommand): ...@@ -100,7 +103,7 @@ class Command(BaseCommand):
} }
) )
mode_tally = GeneratedCertificate.objects.filter( mode_tally = GeneratedCertificate.objects.filter( # pylint: disable=no-member
course_id__exact=course_id, course_id__exact=course_id,
status__exact='downloadable' status__exact='downloadable'
).values('mode').annotate( ).values('mode').annotate(
......
...@@ -81,7 +81,7 @@ class Command(BaseCommand): ...@@ -81,7 +81,7 @@ class Command(BaseCommand):
# Retrieve the IDs of generated certificates with # Retrieve the IDs of generated certificates with
# error status in the set of courses we're considering. # error status in the set of courses we're considering.
queryset = ( queryset = (
GeneratedCertificate.objects.select_related('user') GeneratedCertificate.objects.select_related('user') # pylint: disable=no-member
).filter(status=CertificateStatuses.error) ).filter(status=CertificateStatuses.error)
if only_course_keys: if only_course_keys:
queryset = queryset.filter(course_id__in=only_course_keys) queryset = queryset.filter(course_id__in=only_course_keys)
......
...@@ -86,6 +86,8 @@ class CertificateStatuses(object): ...@@ -86,6 +86,8 @@ class CertificateStatuses(object):
restricted = 'restricted' restricted = 'restricted'
unavailable = 'unavailable' unavailable = 'unavailable'
auditing = 'auditing' auditing = 'auditing'
audit_passing = 'audit_passing'
audit_notpassing = 'audit_notpassing'
readable_statuses = { readable_statuses = {
downloadable: "already received", downloadable: "already received",
...@@ -143,7 +145,7 @@ class CertificateWhitelist(models.Model): ...@@ -143,7 +145,7 @@ class CertificateWhitelist(models.Model):
if student: if student:
white_list = white_list.filter(user=student) white_list = white_list.filter(user=student)
result = [] result = []
generated_certificates = GeneratedCertificate.objects.filter( generated_certificates = GeneratedCertificate.eligible_certificates.filter(
course_id=course_id, course_id=course_id,
user__in=[exception.user for exception in white_list], user__in=[exception.user for exception in white_list],
status=CertificateStatuses.downloadable status=CertificateStatuses.downloadable
...@@ -168,11 +170,42 @@ class CertificateWhitelist(models.Model): ...@@ -168,11 +170,42 @@ class CertificateWhitelist(models.Model):
return result return result
class EligibleCertificateManager(models.Manager):
"""
A manager for `GeneratedCertificate` models that automatically
filters out ineligible certs.
The idea is to prevent accidentally granting certificates to
students who have not enrolled in a cert-granting mode. The
alternative is to filter by eligible_for_certificate=True every
time certs are searched for, which is verbose and likely to be
forgotten.
"""
def get_queryset(self):
"""
Return a queryset for `GeneratedCertificate` models, filtering out
ineligible certificates.
"""
return super(EligibleCertificateManager, self).get_queryset().exclude(
status__in=(CertificateStatuses.audit_passing, CertificateStatuses.audit_notpassing)
)
class GeneratedCertificate(models.Model): class GeneratedCertificate(models.Model):
""" """
Base model for generated certificates Base model for generated certificates
""" """
# Only returns eligible certificates. This should be used in
# preference to the default `objects` manager in most cases.
eligible_certificates = EligibleCertificateManager()
# Normal object manager, which should only be used when ineligible
# certificates (i.e. new audit certs) should be included in the
# results. Django requires us to explicitly declare this.
objects = models.Manager()
MODES = Choices('verified', 'honor', 'audit', 'professional', 'no-id-professional') MODES = Choices('verified', 'honor', 'audit', 'professional', 'no-id-professional')
VERIFIED_CERTS_MODES = [CourseMode.VERIFIED, CourseMode.CREDIT_MODE] VERIFIED_CERTS_MODES = [CourseMode.VERIFIED, CourseMode.CREDIT_MODE]
...@@ -348,7 +381,7 @@ def certificate_status_for_student(student, course_id): ...@@ -348,7 +381,7 @@ def certificate_status_for_student(student, course_id):
''' '''
try: try:
generated_certificate = GeneratedCertificate.objects.get( generated_certificate = GeneratedCertificate.objects.get( # pylint: disable=no-member
user=student, course_id=course_id) user=student, course_id=course_id)
cert_status = { cert_status = {
'status': generated_certificate.status, 'status': generated_certificate.status,
......
...@@ -20,6 +20,7 @@ from student.models import UserProfile, CourseEnrollment ...@@ -20,6 +20,7 @@ from student.models import UserProfile, CourseEnrollment
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from certificates.models import ( from certificates.models import (
CertificateStatuses,
GeneratedCertificate, GeneratedCertificate,
certificate_status_for_student, certificate_status_for_student,
CertificateStatuses as status, CertificateStatuses as status,
...@@ -120,14 +121,14 @@ class XQueueCertInterface(object): ...@@ -120,14 +121,14 @@ class XQueueCertInterface(object):
Change the certificate status to unavailable (if it exists) and request Change the certificate status to unavailable (if it exists) and request
grading. Passing grades will put a certificate request on the queue. grading. Passing grades will put a certificate request on the queue.
Return the status object. Return the certificate.
""" """
# TODO: when del_cert is implemented and plumbed through certificates # TODO: when del_cert is implemented and plumbed through certificates
# repo also, do a deletion followed by a creation r/t a simple # repo also, do a deletion followed by a creation r/t a simple
# recreation. XXX: this leaves orphan cert files laying around in # recreation. XXX: this leaves orphan cert files laying around in
# AWS. See note in the docstring too. # AWS. See note in the docstring too.
try: try:
certificate = GeneratedCertificate.objects.get(user=student, course_id=course_id) certificate = GeneratedCertificate.eligible_certificates.get(user=student, course_id=course_id)
LOGGER.info( LOGGER.info(
( (
...@@ -183,8 +184,7 @@ class XQueueCertInterface(object): ...@@ -183,8 +184,7 @@ class XQueueCertInterface(object):
raise NotImplementedError raise NotImplementedError
# pylint: disable=too-many-statements # pylint: disable=too-many-statements
def add_cert(self, student, course_id, course=None, forced_grade=None, template_file=None, def add_cert(self, student, course_id, course=None, forced_grade=None, template_file=None, generate_pdf=True):
title='None', generate_pdf=True):
""" """
Request a new certificate for a student. Request a new certificate for a student.
...@@ -211,7 +211,7 @@ class XQueueCertInterface(object): ...@@ -211,7 +211,7 @@ class XQueueCertInterface(object):
If a student does not have a passing grade the status If a student does not have a passing grade the status
will change to status.notpassing will change to status.notpassing
Returns the student's status and newly created certificate instance Returns the newly created certificate instance
""" """
valid_statuses = [ valid_statuses = [
...@@ -220,11 +220,13 @@ class XQueueCertInterface(object): ...@@ -220,11 +220,13 @@ class XQueueCertInterface(object):
status.deleted, status.deleted,
status.error, status.error,
status.notpassing, status.notpassing,
status.downloadable status.downloadable,
status.auditing,
status.audit_passing,
status.audit_notpassing,
] ]
cert_status = certificate_status_for_student(student, course_id)['status'] cert_status = certificate_status_for_student(student, course_id)['status']
new_status = cert_status
cert = None cert = None
if cert_status not in valid_statuses: if cert_status not in valid_statuses:
...@@ -239,169 +241,193 @@ class XQueueCertInterface(object): ...@@ -239,169 +241,193 @@ class XQueueCertInterface(object):
cert_status, cert_status,
unicode(valid_statuses) unicode(valid_statuses)
) )
return None
# The caller can optionally pass a course in to avoid
# re-fetching it from Mongo. If they have not provided one,
# get it from the modulestore.
if course is None:
course = modulestore().get_course(course_id, depth=0)
profile = UserProfile.objects.get(user=student)
profile_name = profile.name
# Needed for access control in grading.
self.request.user = student
self.request.session = {}
is_whitelisted = self.whitelist.filter(user=student, course_id=course_id, whitelist=True).exists()
grade = grades.grade(student, self.request, course)
enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(student, course_id)
mode_is_verified = enrollment_mode in GeneratedCertificate.VERIFIED_CERTS_MODES
user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student)
cert_mode = enrollment_mode
is_eligible_for_certificate = is_whitelisted or CourseMode.is_eligible_for_certificate(enrollment_mode)
# For credit mode generate verified certificate
if cert_mode == CourseMode.CREDIT_MODE:
cert_mode = CourseMode.VERIFIED
if template_file is not None:
template_pdf = template_file
elif mode_is_verified and user_is_verified:
template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format(id=course_id)
elif mode_is_verified and not user_is_verified:
template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id)
cert_mode = GeneratedCertificate.MODES.honor
else: else:
# grade the student # honor code and audit students
template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id)
# re-use the course passed in optionally so we don't have to re-fetch everything if forced_grade:
# for every student grade['grade'] = forced_grade
if course is None:
course = modulestore().get_course(course_id, depth=0) cert, created = GeneratedCertificate.objects.get_or_create(user=student, course_id=course_id) # pylint: disable=no-member
profile = UserProfile.objects.get(user=student)
profile_name = profile.name cert.mode = cert_mode
cert.user = student
# Needed cert.grade = grade['percent']
self.request.user = student cert.course_id = course_id
self.request.session = {} cert.name = profile_name
cert.download_url = ''
course_name = course.display_name or unicode(course_id)
is_whitelisted = self.whitelist.filter(user=student, course_id=course_id, whitelist=True).exists() # Strip HTML from grade range label
grade = grades.grade(student, self.request, course) grade_contents = grade.get('grade', None)
enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(student, course_id) try:
mode_is_verified = enrollment_mode in GeneratedCertificate.VERIFIED_CERTS_MODES grade_contents = lxml.html.fromstring(grade_contents).text_content()
user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student) passing = True
cert_mode = enrollment_mode except (TypeError, XMLSyntaxError, ParserError) as exc:
LOGGER.info(
# For credit mode generate verified certificate (
if cert_mode == CourseMode.CREDIT_MODE: u"Could not retrieve grade for student %s "
cert_mode = CourseMode.VERIFIED u"in the course '%s' "
u"because an exception occurred while parsing the "
if mode_is_verified and user_is_verified: u"grade contents '%s' as HTML. "
template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format(id=course_id) u"The exception was: '%s'"
elif mode_is_verified and not user_is_verified: ),
template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id) student.id,
cert_mode = GeneratedCertificate.MODES.honor unicode(course_id),
else: grade_contents,
# honor code and audit students unicode(exc)
template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id) )
if forced_grade:
grade['grade'] = forced_grade # Log if the student is whitelisted
if is_whitelisted:
cert, __ = GeneratedCertificate.objects.get_or_create(user=student, course_id=course_id)
cert.mode = cert_mode
cert.user = student
cert.grade = grade['percent']
cert.course_id = course_id
cert.name = profile_name
cert.download_url = ''
# Strip HTML from grade range label
grade_contents = grade.get('grade', None)
try:
grade_contents = lxml.html.fromstring(grade_contents).text_content()
except (TypeError, XMLSyntaxError, ParserError) as exc:
LOGGER.info( LOGGER.info(
( u"Student %s is whitelisted in '%s'",
u"Could not retrieve grade for student %s "
u"in the course '%s' "
u"because an exception occurred while parsing the "
u"grade contents '%s' as HTML. "
u"The exception was: '%s'"
),
student.id, student.id,
unicode(course_id), unicode(course_id)
grade_contents,
unicode(exc)
) )
passing = True
# Despite blowing up the xml parser, bad values here are fine
grade_contents = None
if is_whitelisted or grade_contents is not None:
if is_whitelisted:
LOGGER.info(
u"Student %s is whitelisted in '%s'",
student.id,
unicode(course_id)
)
# check to see whether the student is on the
# the embargoed country restricted list
# otherwise, put a new certificate request
# on the queue
if self.restricted.filter(user=student).exists():
new_status = status.restricted
cert.status = new_status
cert.save()
LOGGER.info(
(
u"Student %s is in the embargoed country restricted "
u"list, so their certificate status has been set to '%s' "
u"for the course '%s'. "
u"No certificate generation task was sent to the XQueue."
),
student.id,
new_status,
unicode(course_id)
)
else:
key = make_hashkey(random.random())
cert.key = key
contents = {
'action': 'create',
'username': student.username,
'course_id': unicode(course_id),
'course_name': course_name,
'name': profile_name,
'grade': grade_contents,
'template_pdf': template_pdf,
}
if template_file:
contents['template_pdf'] = template_file
if generate_pdf:
new_status = status.generating
else:
new_status = status.downloadable
cert.verify_uuid = uuid4().hex
cert.status = new_status
cert.save()
if generate_pdf:
try:
self._send_to_xqueue(contents, key)
except XQueueAddToQueueError as exc:
new_status = ExampleCertificate.STATUS_ERROR
cert.status = new_status
cert.error_reason = unicode(exc)
cert.save()
LOGGER.critical(
(
u"Could not add certificate task to XQueue. "
u"The course was '%s' and the student was '%s'."
u"The certificate task status has been marked as 'error' "
u"and can be re-submitted with a management command."
), course_id, student.id
)
else:
LOGGER.info(
(
u"The certificate status has been set to '%s'. "
u"Sent a certificate grading task to the XQueue "
u"with the key '%s'. "
),
new_status,
key
)
else: else:
new_status = status.notpassing passing = False
cert.status = new_status
cert.save() # If this user's enrollment is not eligible to receive a
# certificate, mark it as such for reporting and
# analytics. Only do this if the certificate is new, or
# already marked as ineligible -- we don't want to mark
# existing audit certs as ineligible.
cutoff = settings.AUDIT_CERT_CUTOFF_DATE
if (cutoff and cert.created_date >= cutoff) and not is_eligible_for_certificate:
cert.status = CertificateStatuses.audit_passing if passing else CertificateStatuses.audit_notpassing
cert.save()
LOGGER.info(
u"Student %s with enrollment mode %s is not eligible for a certificate.",
student.id,
enrollment_mode
)
return cert
# If they are not passing, short-circuit and don't generate cert
elif not passing:
cert.status = status.notpassing
cert.save()
LOGGER.info(
(
u"Student %s does not have a grade for '%s', "
u"so their certificate status has been set to '%s'. "
u"No certificate generation task was sent to the XQueue."
),
student.id,
unicode(course_id),
cert.status
)
return cert
# Check to see whether the student is on the the embargoed
# country restricted list. If so, they should not receive a
# certificate -- set their status to restricted and log it.
if self.restricted.filter(user=student).exists():
cert.status = status.restricted
cert.save()
LOGGER.info(
(
u"Student %s is in the embargoed country restricted "
u"list, so their certificate status has been set to '%s' "
u"for the course '%s'. "
u"No certificate generation task was sent to the XQueue."
),
student.id,
cert.status,
unicode(course_id)
)
return cert
# Finally, generate the certificate and send it off.
return self._generate_cert(cert, course, student, grade_contents, template_pdf, generate_pdf)
def _generate_cert(self, cert, course, student, grade_contents, template_pdf, generate_pdf):
"""
Generate a certificate for the student. If `generate_pdf` is True,
sends a request to XQueue.
"""
course_id = unicode(course.id)
key = make_hashkey(random.random())
cert.key = key
contents = {
'action': 'create',
'username': student.username,
'course_id': course_id,
'course_name': course.display_name or course_id,
'name': cert.name,
'grade': grade_contents,
'template_pdf': template_pdf,
}
if generate_pdf:
cert.status = status.generating
else:
cert.status = status.downloadable
cert.verify_uuid = uuid4().hex
cert.save()
if generate_pdf:
try:
self._send_to_xqueue(contents, key)
except XQueueAddToQueueError as exc:
cert.status = ExampleCertificate.STATUS_ERROR
cert.error_reason = unicode(exc)
cert.save()
LOGGER.critical(
(
u"Could not add certificate task to XQueue. "
u"The course was '%s' and the student was '%s'."
u"The certificate task status has been marked as 'error' "
u"and can be re-submitted with a management command."
), course_id, student.id
)
else:
LOGGER.info( LOGGER.info(
( (
u"Student %s does not have a grade for '%s', " u"The certificate status has been set to '%s'. "
u"so their certificate status has been set to '%s'. " u"Sent a certificate grading task to the XQueue "
u"No certificate generation task was sent to the XQueue." u"with the key '%s'. "
), ),
student.id, cert.status,
unicode(course_id), key
new_status
) )
return cert
return new_status, cert
def add_example_cert(self, example_cert): def add_example_cert(self, example_cert):
"""Add a task to create an example certificate. """Add a task to create an example certificate.
......
...@@ -228,7 +228,7 @@ class GenerateUserCertificatesTest(EventTestMixin, WebCertificateTestMixin, Modu ...@@ -228,7 +228,7 @@ class GenerateUserCertificatesTest(EventTestMixin, WebCertificateTestMixin, Modu
certs_api.generate_user_certificates(self.student, self.course.id) certs_api.generate_user_certificates(self.student, self.course.id)
# Verify that the certificate has status 'generating' # Verify that the certificate has status 'generating'
cert = GeneratedCertificate.objects.get(user=self.student, course_id=self.course.id) cert = GeneratedCertificate.eligible_certificates.get(user=self.student, course_id=self.course.id)
self.assertEqual(cert.status, CertificateStatuses.generating) self.assertEqual(cert.status, CertificateStatuses.generating)
self.assert_event_emitted( self.assert_event_emitted(
'edx.certificate.created', 'edx.certificate.created',
...@@ -246,7 +246,7 @@ class GenerateUserCertificatesTest(EventTestMixin, WebCertificateTestMixin, Modu ...@@ -246,7 +246,7 @@ class GenerateUserCertificatesTest(EventTestMixin, WebCertificateTestMixin, Modu
certs_api.generate_user_certificates(self.student, self.course.id) certs_api.generate_user_certificates(self.student, self.course.id)
# Verify that the certificate has been marked with status error # Verify that the certificate has been marked with status error
cert = GeneratedCertificate.objects.get(user=self.student, course_id=self.course.id) cert = GeneratedCertificate.eligible_certificates.get(user=self.student, course_id=self.course.id)
self.assertEqual(cert.status, 'error') self.assertEqual(cert.status, 'error')
self.assertIn(self.ERROR_REASON, cert.error_reason) self.assertIn(self.ERROR_REASON, cert.error_reason)
...@@ -260,7 +260,7 @@ class GenerateUserCertificatesTest(EventTestMixin, WebCertificateTestMixin, Modu ...@@ -260,7 +260,7 @@ class GenerateUserCertificatesTest(EventTestMixin, WebCertificateTestMixin, Modu
certs_api.generate_user_certificates(self.student, self.course.id) certs_api.generate_user_certificates(self.student, self.course.id)
# Verify that the certificate has status 'downloadable' # Verify that the certificate has status 'downloadable'
cert = GeneratedCertificate.objects.get(user=self.student, course_id=self.course.id) cert = GeneratedCertificate.eligible_certificates.get(user=self.student, course_id=self.course.id)
self.assertEqual(cert.status, CertificateStatuses.downloadable) self.assertEqual(cert.status, CertificateStatuses.downloadable)
@patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': False}) @patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': False})
......
...@@ -6,6 +6,7 @@ from nose.plugins.attrib import attr ...@@ -6,6 +6,7 @@ from nose.plugins.attrib import attr
from django.test.utils import override_settings from django.test.utils import override_settings
from mock import patch from mock import patch
from course_modes.models import CourseMode
from opaque_keys.edx.locator import CourseLocator from opaque_keys.edx.locator import CourseLocator
from certificates.tests.factories import BadgeAssertionFactory from certificates.tests.factories import BadgeAssertionFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
...@@ -30,16 +31,17 @@ class CertificateManagementTest(ModuleStoreTestCase): ...@@ -30,16 +31,17 @@ class CertificateManagementTest(ModuleStoreTestCase):
for __ in range(3) for __ in range(3)
] ]
def _create_cert(self, course_key, user, status): def _create_cert(self, course_key, user, status, mode=CourseMode.HONOR):
"""Create a certificate entry. """ """Create a certificate entry. """
# Enroll the user in the course # Enroll the user in the course
CourseEnrollmentFactory.create( CourseEnrollmentFactory.create(
user=user, user=user,
course_id=course_key course_id=course_key,
mode=mode
) )
# Create the certificate # Create the certificate
GeneratedCertificate.objects.create( GeneratedCertificate.eligible_certificates.create(
user=user, user=user,
course_id=course_key, course_id=course_key,
status=status status=status
...@@ -52,7 +54,7 @@ class CertificateManagementTest(ModuleStoreTestCase): ...@@ -52,7 +54,7 @@ class CertificateManagementTest(ModuleStoreTestCase):
def _assert_cert_status(self, course_key, user, expected_status): def _assert_cert_status(self, course_key, user, expected_status):
"""Check the status of a certificate. """ """Check the status of a certificate. """
cert = GeneratedCertificate.objects.get(user=user, course_id=course_key) cert = GeneratedCertificate.eligible_certificates.get(user=user, course_id=course_key)
self.assertEqual(cert.status, expected_status) self.assertEqual(cert.status, expected_status)
...@@ -61,9 +63,10 @@ class CertificateManagementTest(ModuleStoreTestCase): ...@@ -61,9 +63,10 @@ class CertificateManagementTest(ModuleStoreTestCase):
class ResubmitErrorCertificatesTest(CertificateManagementTest): class ResubmitErrorCertificatesTest(CertificateManagementTest):
"""Tests for the resubmit_error_certificates management command. """ """Tests for the resubmit_error_certificates management command. """
def test_resubmit_error_certificate(self): @ddt.data(CourseMode.HONOR, CourseMode.VERIFIED)
def test_resubmit_error_certificate(self, mode):
# Create a certificate with status 'error' # Create a certificate with status 'error'
self._create_cert(self.courses[0].id, self.user, CertificateStatuses.error) self._create_cert(self.courses[0].id, self.user, CertificateStatuses.error, mode)
# Re-submit all certificates with status 'error' # Re-submit all certificates with status 'error'
with check_mongo_calls(1): with check_mongo_calls(1):
...@@ -198,7 +201,7 @@ class RegenerateCertificatesTest(CertificateManagementTest): ...@@ -198,7 +201,7 @@ class RegenerateCertificatesTest(CertificateManagementTest):
username=self.user.email, course=unicode(key), noop=False, insecure=True, template_file=None, username=self.user.email, course=unicode(key), noop=False, insecure=True, template_file=None,
grade_value=None grade_value=None
) )
certificate = GeneratedCertificate.objects.get( certificate = GeneratedCertificate.eligible_certificates.get(
user=self.user, user=self.user,
course_id=key course_id=key
) )
...@@ -236,7 +239,7 @@ class UngenerateCertificatesTest(CertificateManagementTest): ...@@ -236,7 +239,7 @@ class UngenerateCertificatesTest(CertificateManagementTest):
course=unicode(key), noop=False, insecure=True, force=False course=unicode(key), noop=False, insecure=True, force=False
) )
self.assertTrue(mock_send_to_queue.called) self.assertTrue(mock_send_to_queue.called)
certificate = GeneratedCertificate.objects.get( certificate = GeneratedCertificate.eligible_certificates.get(
user=self.user, user=self.user,
course_id=key course_id=key
) )
......
...@@ -28,7 +28,7 @@ class CreateFakeCertTest(TestCase): ...@@ -28,7 +28,7 @@ class CreateFakeCertTest(TestCase):
cert_mode='verified', cert_mode='verified',
grade='0.89' grade='0.89'
) )
cert = GeneratedCertificate.objects.get(user=self.user, course_id=self.COURSE_KEY) cert = GeneratedCertificate.eligible_certificates.get(user=self.user, course_id=self.COURSE_KEY)
self.assertEqual(cert.status, 'downloadable') self.assertEqual(cert.status, 'downloadable')
self.assertEqual(cert.mode, 'verified') self.assertEqual(cert.mode, 'verified')
self.assertEqual(cert.grade, '0.89') self.assertEqual(cert.grade, '0.89')
...@@ -41,7 +41,7 @@ class CreateFakeCertTest(TestCase): ...@@ -41,7 +41,7 @@ class CreateFakeCertTest(TestCase):
unicode(self.COURSE_KEY), unicode(self.COURSE_KEY),
cert_mode='honor' cert_mode='honor'
) )
cert = GeneratedCertificate.objects.get(user=self.user, course_id=self.COURSE_KEY) cert = GeneratedCertificate.eligible_certificates.get(user=self.user, course_id=self.COURSE_KEY)
self.assertEqual(cert.mode, 'honor') self.assertEqual(cert.mode, 'honor')
def test_too_few_args(self): def test_too_few_args(self):
......
...@@ -8,13 +8,21 @@ from django.test.utils import override_settings ...@@ -8,13 +8,21 @@ from django.test.utils import override_settings
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from path import Path as path from path import Path as path
from opaque_keys.edx.locator import CourseLocator
from certificates.models import ( from certificates.models import (
ExampleCertificate, ExampleCertificate,
ExampleCertificateSet, ExampleCertificateSet,
CertificateHtmlViewConfiguration, CertificateHtmlViewConfiguration,
CertificateTemplateAsset, CertificateTemplateAsset,
BadgeImageConfiguration) BadgeImageConfiguration,
EligibleCertificateManager,
GeneratedCertificate,
CertificateStatuses,
)
from certificates.tests.factories import GeneratedCertificateFactory
from opaque_keys.edx.locator import CourseLocator
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
FEATURES_INVALID_FILE_PATH = settings.FEATURES.copy() FEATURES_INVALID_FILE_PATH = settings.FEATURES.copy()
FEATURES_INVALID_FILE_PATH['CERTS_HTML_VIEW_CONFIG_PATH'] = 'invalid/path/to/config.json' FEATURES_INVALID_FILE_PATH['CERTS_HTML_VIEW_CONFIG_PATH'] = 'invalid/path/to/config.json'
...@@ -234,3 +242,42 @@ class CertificateTemplateAssetTest(TestCase): ...@@ -234,3 +242,42 @@ class CertificateTemplateAssetTest(TestCase):
certificate_template_asset = CertificateTemplateAsset.objects.get(id=1) certificate_template_asset = CertificateTemplateAsset.objects.get(id=1)
self.assertEqual(certificate_template_asset.asset, 'certificate_template_assets/1/picture2.jpg') self.assertEqual(certificate_template_asset.asset, 'certificate_template_assets/1/picture2.jpg')
@attr('shard_1')
class EligibleCertificateManagerTest(SharedModuleStoreTestCase):
"""
Test the GeneratedCertificate model's object manager for filtering
out ineligible certs.
"""
@classmethod
def setUpClass(cls):
super(EligibleCertificateManagerTest, cls).setUpClass()
cls.courses = (CourseFactory(), CourseFactory())
def setUp(self):
super(EligibleCertificateManagerTest, self).setUp()
self.user = UserFactory()
self.eligible_cert = GeneratedCertificateFactory.create(
status=CertificateStatuses.downloadable,
user=self.user,
course_id=self.courses[0].id # pylint: disable=no-member
)
self.ineligible_cert = GeneratedCertificateFactory.create(
status=CertificateStatuses.audit_passing,
user=self.user,
course_id=self.courses[1].id # pylint: disable=no-member
)
def test_filter_ineligible_certificates(self):
"""
Verify that the EligibleCertificateManager filters out
certificates marked as ineligible, and that the default object
manager for GeneratedCertificate does not filter them out.
"""
self.assertEqual(list(GeneratedCertificate.eligible_certificates.filter(user=self.user)), [self.eligible_cert])
self.assertEqual(
list(GeneratedCertificate.objects.filter(user=self.user)), # pylint: disable=no-member
[self.eligible_cert, self.ineligible_cert]
)
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Tests for the XQueue certificates interface. """ """Tests for the XQueue certificates interface. """
from contextlib import contextmanager from contextlib import contextmanager
from datetime import datetime, timedelta
import ddt import ddt
import json import json
from mock import patch, Mock from mock import patch, Mock
...@@ -8,7 +9,10 @@ from nose.plugins.attrib import attr ...@@ -8,7 +9,10 @@ from nose.plugins.attrib import attr
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
import freezegun
import pytz
from course_modes.models import CourseMode
from opaque_keys.edx.locator import CourseLocator from opaque_keys.edx.locator import CourseLocator
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from student.tests.factories import UserFactory, CourseEnrollmentFactory from student.tests.factories import UserFactory, CourseEnrollmentFactory
...@@ -22,13 +26,14 @@ from xmodule.modulestore.tests.factories import CourseFactory ...@@ -22,13 +26,14 @@ from xmodule.modulestore.tests.factories import CourseFactory
# in our `XQueueCertInterface` implementation. # in our `XQueueCertInterface` implementation.
from capa.xqueue_interface import XQueueInterface from capa.xqueue_interface import XQueueInterface
from certificates.queue import XQueueCertInterface
from certificates.models import ( from certificates.models import (
ExampleCertificateSet, ExampleCertificateSet,
ExampleCertificate, ExampleCertificate,
GeneratedCertificate, GeneratedCertificate,
CertificateStatuses, CertificateStatuses,
) )
from certificates.queue import XQueueCertInterface
from certificates.tests.factories import CertificateWhitelistFactory, GeneratedCertificateFactory
from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory
...@@ -74,17 +79,22 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase): ...@@ -74,17 +79,22 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase):
# Verify that add_cert method does not add message to queue # Verify that add_cert method does not add message to queue
self.assertFalse(mock_send.called) self.assertFalse(mock_send.called)
certificate = GeneratedCertificate.objects.get(user=self.user, course_id=self.course.id) certificate = GeneratedCertificate.eligible_certificates.get(user=self.user, course_id=self.course.id)
self.assertEqual(certificate.status, CertificateStatuses.downloadable) self.assertEqual(certificate.status, CertificateStatuses.downloadable)
self.assertIsNotNone(certificate.verify_uuid) self.assertIsNotNone(certificate.verify_uuid)
@ddt.data('honor', 'audit') @ddt.data('honor', 'audit')
@override_settings(AUDIT_CERT_CUTOFF_DATE=datetime.now(pytz.UTC) - timedelta(days=1))
def test_add_cert_with_honor_certificates(self, mode): def test_add_cert_with_honor_certificates(self, mode):
"""Test certificates generations for honor and audit modes.""" """Test certificates generations for honor and audit modes."""
template_name = 'certificate-template-{id.org}-{id.course}.pdf'.format( template_name = 'certificate-template-{id.org}-{id.course}.pdf'.format(
id=self.course.id id=self.course.id
) )
self.assert_queue_response(mode, mode, template_name) mock_send = self.add_cert_to_queue(mode)
if CourseMode.is_eligible_for_certificate(mode):
self.assert_certificate_generated(mock_send, mode, template_name)
else:
self.assert_ineligible_certificate_generated(mock_send, mode)
@ddt.data('credit', 'verified') @ddt.data('credit', 'verified')
def test_add_cert_with_verified_certificates(self, mode): def test_add_cert_with_verified_certificates(self, mode):
...@@ -95,10 +105,40 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase): ...@@ -95,10 +105,40 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase):
id=self.course.id id=self.course.id
) )
self.assert_queue_response(mode, 'verified', template_name) mock_send = self.add_cert_to_queue(mode)
self.assert_certificate_generated(mock_send, 'verified', template_name)
def assert_queue_response(self, mode, expected_mode, expected_template_name): def test_ineligible_cert_whitelisted(self):
"""Dry method for course enrollment and adding request to queue.""" """Test that audit mode students can receive a certificate if they are whitelisted."""
# Enroll as audit
CourseEnrollmentFactory(
user=self.user_2,
course_id=self.course.id,
is_active=True,
mode='audit'
)
# Whitelist student
CertificateWhitelistFactory(course_id=self.course.id, user=self.user_2)
# Generate certs
with patch('courseware.grades.grade', Mock(return_value={'grade': 'Pass', 'percent': 0.75})):
with patch.object(XQueueInterface, 'send_to_queue') as mock_send:
mock_send.return_value = (0, None)
self.xqueue.add_cert(self.user_2, self.course.id)
# Assert cert generated correctly
self.assertTrue(mock_send.called)
certificate = GeneratedCertificate.certificate_for_student(self.user_2, self.course.id)
self.assertIsNotNone(certificate)
self.assertEqual(certificate.mode, 'audit')
def add_cert_to_queue(self, mode):
"""
Dry method for course enrollment and adding request to
queue. Returns a mock object containing information about the
`XQueueInterface.send_to_queue` method, which can be used in other
assertions.
"""
CourseEnrollmentFactory( CourseEnrollmentFactory(
user=self.user_2, user=self.user_2,
course_id=self.course.id, course_id=self.course.id,
...@@ -109,19 +149,137 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase): ...@@ -109,19 +149,137 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase):
with patch.object(XQueueInterface, 'send_to_queue') as mock_send: with patch.object(XQueueInterface, 'send_to_queue') as mock_send:
mock_send.return_value = (0, None) mock_send.return_value = (0, None)
self.xqueue.add_cert(self.user_2, self.course.id) self.xqueue.add_cert(self.user_2, self.course.id)
return mock_send
def assert_certificate_generated(self, mock_send, expected_mode, expected_template_name):
"""
Assert that a certificate was generated with the correct mode and
template type.
"""
# Verify that the task was sent to the queue with the correct callback URL # Verify that the task was sent to the queue with the correct callback URL
self.assertTrue(mock_send.called) self.assertTrue(mock_send.called)
__, kwargs = mock_send.call_args_list[0] __, kwargs = mock_send.call_args_list[0]
actual_header = json.loads(kwargs['header']) actual_header = json.loads(kwargs['header'])
self.assertIn('https://edx.org/update_certificate?key=', actual_header['lms_callback_url']) self.assertIn('https://edx.org/update_certificate?key=', actual_header['lms_callback_url'])
certificate = GeneratedCertificate.objects.get(user=self.user_2, course_id=self.course.id)
self.assertEqual(certificate.mode, expected_mode)
body = json.loads(kwargs['body']) body = json.loads(kwargs['body'])
self.assertIn(expected_template_name, body['template_pdf']) self.assertIn(expected_template_name, body['template_pdf'])
certificate = GeneratedCertificate.eligible_certificates.get(user=self.user_2, course_id=self.course.id)
self.assertEqual(certificate.mode, expected_mode)
def assert_ineligible_certificate_generated(self, mock_send, expected_mode):
"""
Assert that an ineligible certificate was generated with the
correct mode.
"""
# Ensure the certificate was not generated
self.assertFalse(mock_send.called)
certificate = GeneratedCertificate.objects.get( # pylint: disable=no-member
user=self.user_2,
course_id=self.course.id
)
self.assertIn(certificate.status, (CertificateStatuses.audit_passing, CertificateStatuses.audit_notpassing))
self.assertEqual(certificate.mode, expected_mode)
@ddt.data(
(CertificateStatuses.restricted, False),
(CertificateStatuses.deleting, False),
(CertificateStatuses.generating, True),
(CertificateStatuses.unavailable, True),
(CertificateStatuses.deleted, True),
(CertificateStatuses.error, True),
(CertificateStatuses.notpassing, True),
(CertificateStatuses.downloadable, True),
(CertificateStatuses.auditing, True),
)
@ddt.unpack
def test_add_cert_statuses(self, status, should_generate):
"""
Test that certificates can or cannot be generated with the given
certificate status.
"""
with patch('certificates.queue.certificate_status_for_student', Mock(return_value={'status': status})):
mock_send = self.add_cert_to_queue('verified')
if should_generate:
self.assertTrue(mock_send.called)
else:
self.assertFalse(mock_send.called)
@ddt.data(
# Eligible and should stay that way
(
CertificateStatuses.downloadable,
datetime.now(pytz.UTC) - timedelta(days=2),
'Pass',
CertificateStatuses.generating
),
# Ensure that certs in the wrong state can be fixed by regeneration
(
CertificateStatuses.downloadable,
datetime.now(pytz.UTC) - timedelta(hours=1),
'Pass',
CertificateStatuses.audit_passing
),
# Ineligible and should stay that way
(
CertificateStatuses.audit_passing,
datetime.now(pytz.UTC) - timedelta(hours=1),
'Pass',
CertificateStatuses.audit_passing
),
# As above
(
CertificateStatuses.audit_notpassing,
datetime.now(pytz.UTC) - timedelta(hours=1),
'Pass',
CertificateStatuses.audit_passing
),
# As above
(
CertificateStatuses.audit_notpassing,
datetime.now(pytz.UTC) - timedelta(hours=1),
None,
CertificateStatuses.audit_notpassing
),
)
@ddt.unpack
@override_settings(AUDIT_CERT_CUTOFF_DATE=datetime.now(pytz.UTC) - timedelta(days=1))
def test_regen_audit_certs_eligibility(self, status, created_date, grade, expected_status):
"""
Test that existing audit certificates remain eligible even if cert
generation is re-run.
"""
# Create an existing audit enrollment and certificate
CourseEnrollmentFactory(
user=self.user_2,
course_id=self.course.id,
is_active=True,
mode=CourseMode.AUDIT,
)
with freezegun.freeze_time(created_date):
GeneratedCertificateFactory(
user=self.user_2,
course_id=self.course.id,
grade='1.0',
status=status,
mode=GeneratedCertificate.MODES.audit,
)
# Run grading/cert generation again
with patch('courseware.grades.grade', Mock(return_value={'grade': grade, 'percent': 0.75})):
with patch.object(XQueueInterface, 'send_to_queue') as mock_send:
mock_send.return_value = (0, None)
self.xqueue.add_cert(self.user_2, self.course.id)
self.assertEqual(
GeneratedCertificate.objects.get(user=self.user_2, course_id=self.course.id).status, # pylint: disable=no-member
expected_status
)
@attr('shard_1') @attr('shard_1')
@override_settings(CERT_QUEUE='certificates') @override_settings(CERT_QUEUE='certificates')
......
...@@ -64,7 +64,7 @@ class CertificateSupportTestCase(TestCase): ...@@ -64,7 +64,7 @@ class CertificateSupportTestCase(TestCase):
) )
# Create certificates for the student # Create certificates for the student
self.cert = GeneratedCertificate.objects.create( self.cert = GeneratedCertificate.eligible_certificates.create(
user=self.student, user=self.student,
course_id=self.CERT_COURSE_KEY, course_id=self.CERT_COURSE_KEY,
grade=self.CERT_GRADE, grade=self.CERT_GRADE,
...@@ -244,7 +244,7 @@ class CertificateRegenerateTests(ModuleStoreTestCase, CertificateSupportTestCase ...@@ -244,7 +244,7 @@ class CertificateRegenerateTests(ModuleStoreTestCase, CertificateSupportTestCase
# Check that the user's certificate was updated # Check that the user's certificate was updated
# Since the student hasn't actually passed the course, # Since the student hasn't actually passed the course,
# we'd expect that the certificate status will be "notpassing" # we'd expect that the certificate status will be "notpassing"
cert = GeneratedCertificate.objects.get(user=self.student) cert = GeneratedCertificate.eligible_certificates.get(user=self.student)
self.assertEqual(cert.status, CertificateStatuses.notpassing) self.assertEqual(cert.status, CertificateStatuses.notpassing)
def test_regenerate_certificate_missing_params(self): def test_regenerate_certificate_missing_params(self):
...@@ -283,7 +283,7 @@ class CertificateRegenerateTests(ModuleStoreTestCase, CertificateSupportTestCase ...@@ -283,7 +283,7 @@ class CertificateRegenerateTests(ModuleStoreTestCase, CertificateSupportTestCase
def test_regenerate_user_has_no_certificate(self): def test_regenerate_user_has_no_certificate(self):
# Delete the user's certificate # Delete the user's certificate
GeneratedCertificate.objects.all().delete() GeneratedCertificate.eligible_certificates.all().delete()
# Should be able to regenerate # Should be able to regenerate
response = self._regenerate( response = self._regenerate(
...@@ -293,7 +293,7 @@ class CertificateRegenerateTests(ModuleStoreTestCase, CertificateSupportTestCase ...@@ -293,7 +293,7 @@ class CertificateRegenerateTests(ModuleStoreTestCase, CertificateSupportTestCase
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# A new certificate is created # A new certificate is created
num_certs = GeneratedCertificate.objects.filter(user=self.student).count() num_certs = GeneratedCertificate.eligible_certificates.filter(user=self.student).count()
self.assertEqual(num_certs, 1) self.assertEqual(num_certs, 1)
def _regenerate(self, course_key=None, username=None): def _regenerate(self, course_key=None, username=None):
......
...@@ -210,7 +210,7 @@ class MicrositeCertificatesViewsTests(ModuleStoreTestCase): ...@@ -210,7 +210,7 @@ class MicrositeCertificatesViewsTests(ModuleStoreTestCase):
self.user.profile.name = "Joe User" self.user.profile.name = "Joe User"
self.user.profile.save() self.user.profile.save()
self.client.login(username=self.user.username, password='foo') self.client.login(username=self.user.username, password='foo')
self.cert = GeneratedCertificate.objects.create( self.cert = GeneratedCertificate.eligible_certificates.create(
user=self.user, user=self.user,
course_id=self.course_id, course_id=self.course_id,
download_uuid=uuid4(), download_uuid=uuid4(),
......
...@@ -13,6 +13,7 @@ from django.core.urlresolvers import reverse ...@@ -13,6 +13,7 @@ from django.core.urlresolvers import reverse
from django.test.client import Client from django.test.client import Client
from django.test.utils import override_settings from django.test.utils import override_settings
from course_modes.models import CourseMode
from openedx.core.lib.tests.assertions.events import assert_event_matches from openedx.core.lib.tests.assertions.events import assert_event_matches
from student.tests.factories import UserFactory, CourseEnrollmentFactory from student.tests.factories import UserFactory, CourseEnrollmentFactory
from student.roles import CourseStaffRole from student.roles import CourseStaffRole
...@@ -95,7 +96,8 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase): ...@@ -95,7 +96,8 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
) )
CourseEnrollmentFactory.create( CourseEnrollmentFactory.create(
user=self.user, user=self.user,
course_id=self.course_id course_id=self.course_id,
mode=CourseMode.HONOR,
) )
CertificateHtmlViewConfigurationFactory.create() CertificateHtmlViewConfigurationFactory.create()
LinkedInAddToProfileConfigurationFactory.create() LinkedInAddToProfileConfigurationFactory.create()
...@@ -360,6 +362,38 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase): ...@@ -360,6 +362,38 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
self.assertIn("Cannot Find Certificate", response.content) self.assertIn("Cannot Find Certificate", response.content)
self.assertIn("We cannot find a certificate with this URL or ID number.", response.content) self.assertIn("We cannot find a certificate with this URL or ID number.", response.content)
@ddt.data(
(CertificateStatuses.downloadable, True),
(CertificateStatuses.audit_passing, False),
(CertificateStatuses.audit_notpassing, False),
)
@ddt.unpack
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
def test_audit_certificate_display(self, status, eligible_for_certificate):
"""
Ensure that audit-mode certs are only shown in the web view if they
are eligible for a certificate.
"""
# Convert the cert to audit, with the specified eligibility
self.cert.mode = 'audit'
self.cert.status = status
self.cert.save()
self._add_course_certificates(count=1, signatory_count=2)
test_url = get_certificate_url(
user_id=self.user.id,
course_id=unicode(self.course.id)
)
response = self.client.get(test_url)
if eligible_for_certificate:
self.assertIn(str(self.cert.verify_uuid), response.content)
else:
self.assertIn("Invalid Certificate", response.content)
self.assertIn("Cannot Find Certificate", response.content)
self.assertIn("We cannot find a certificate with this URL or ID number.", response.content)
self.assertNotIn(str(self.cert.verify_uuid), response.content)
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
def test_html_view_for_invalid_certificate(self): def test_html_view_for_invalid_certificate(self):
""" """
...@@ -515,7 +549,7 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase): ...@@ -515,7 +549,7 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
course_id=unicode(self.course.id) course_id=unicode(self.course.id)
) )
self.cert.delete() self.cert.delete()
self.assertEqual(len(GeneratedCertificate.objects.all()), 0) self.assertEqual(len(GeneratedCertificate.eligible_certificates.all()), 0)
response = self.client.get(test_url) response = self.client.get(test_url)
self.assertIn('invalid', response.content) self.assertIn('invalid', response.content)
...@@ -538,7 +572,7 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase): ...@@ -538,7 +572,7 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
preview mode. Either the certificate is marked active or not. preview mode. Either the certificate is marked active or not.
""" """
self.cert.delete() self.cert.delete()
self.assertEqual(len(GeneratedCertificate.objects.all()), 0) self.assertEqual(len(GeneratedCertificate.eligible_certificates.all()), 0)
self._add_course_certificates(count=1, signatory_count=2) self._add_course_certificates(count=1, signatory_count=2)
test_url = get_certificate_url( test_url = get_certificate_url(
user_id=self.user.id, user_id=self.user.id,
......
...@@ -345,7 +345,7 @@ def _get_user_certificate(request, user, course_key, course, preview_mode=None): ...@@ -345,7 +345,7 @@ def _get_user_certificate(request, user, course_key, course, preview_mode=None):
else: else:
# certificate is being viewed by learner or public # certificate is being viewed by learner or public
try: try:
user_certificate = GeneratedCertificate.objects.get( user_certificate = GeneratedCertificate.eligible_certificates.get(
user=user, user=user,
course_id=course_key, course_id=course_key,
status=CertificateStatuses.downloadable status=CertificateStatuses.downloadable
...@@ -463,7 +463,7 @@ def render_cert_by_uuid(request, certificate_uuid): ...@@ -463,7 +463,7 @@ def render_cert_by_uuid(request, certificate_uuid):
This public view generates an HTML representation of the specified certificate This public view generates an HTML representation of the specified certificate
""" """
try: try:
certificate = GeneratedCertificate.objects.get( certificate = GeneratedCertificate.eligible_certificates.get(
verify_uuid=certificate_uuid, verify_uuid=certificate_uuid,
status=CertificateStatuses.downloadable status=CertificateStatuses.downloadable
) )
......
...@@ -75,7 +75,7 @@ def update_certificate(request): ...@@ -75,7 +75,7 @@ def update_certificate(request):
try: try:
course_key = SlashSeparatedCourseKey.from_deprecated_string(xqueue_body['course_id']) course_key = SlashSeparatedCourseKey.from_deprecated_string(xqueue_body['course_id'])
cert = GeneratedCertificate.objects.get( cert = GeneratedCertificate.eligible_certificates.get(
user__username=xqueue_body['username'], user__username=xqueue_body['username'],
course_id=course_key, course_id=course_key,
key=xqueue_header['lms_key']) key=xqueue_header['lms_key'])
......
...@@ -618,7 +618,7 @@ class CertificateExceptionViewInstructorApiTest(SharedModuleStoreTestCase): ...@@ -618,7 +618,7 @@ class CertificateExceptionViewInstructorApiTest(SharedModuleStoreTestCase):
# Verify that certificate exception successfully removed from CertificateWhitelist and GeneratedCertificate # Verify that certificate exception successfully removed from CertificateWhitelist and GeneratedCertificate
with self.assertRaises(ObjectDoesNotExist): with self.assertRaises(ObjectDoesNotExist):
CertificateWhitelist.objects.get(user=self.user2, course_id=self.course.id) CertificateWhitelist.objects.get(user=self.user2, course_id=self.course.id)
GeneratedCertificate.objects.get( GeneratedCertificate.eligible_certificates.get(
user=self.user2, course_id=self.course.id, status__not=CertificateStatuses.unavailable user=self.user2, course_id=self.course.id, status__not=CertificateStatuses.unavailable
) )
......
...@@ -2819,7 +2819,10 @@ def remove_certificate_exception(course_key, student): ...@@ -2819,7 +2819,10 @@ def remove_certificate_exception(course_key, student):
) )
try: try:
generated_certificate = GeneratedCertificate.objects.get(user=student, course_id=course_key) generated_certificate = GeneratedCertificate.objects.get( # pylint: disable=no-member
user=student,
course_id=course_key
)
generated_certificate.invalidate() generated_certificate.invalidate()
except ObjectDoesNotExist: except ObjectDoesNotExist:
# Certificate has not been generated yet, so just remove the certificate exception from white list # Certificate has not been generated yet, so just remove the certificate exception from white list
......
...@@ -185,7 +185,7 @@ def issued_certificates(course_key, features): ...@@ -185,7 +185,7 @@ def issued_certificates(course_key, features):
report_run_date = datetime.date.today().strftime("%B %d, %Y") report_run_date = datetime.date.today().strftime("%B %d, %Y")
certificate_features = [x for x in CERTIFICATE_FEATURES if x in features] certificate_features = [x for x in CERTIFICATE_FEATURES if x in features]
generated_certificates = list(GeneratedCertificate.objects.filter( generated_certificates = list(GeneratedCertificate.eligible_certificates.filter(
course_id=course_key, course_id=course_key,
status=CertificateStatuses.downloadable status=CertificateStatuses.downloadable
).values(*certificate_features).annotate(total_issued_certificate=Count('mode'))) ).values(*certificate_features).annotate(total_issued_certificate=Count('mode')))
......
...@@ -1580,7 +1580,7 @@ def invalidate_generated_certificates(course_id, enrolled_students, certificate_ ...@@ -1580,7 +1580,7 @@ def invalidate_generated_certificates(course_id, enrolled_students, certificate_
:param enrolled_students: (queryset or list) students enrolled in the course :param enrolled_students: (queryset or list) students enrolled in the course
:param certificate_statuses: certificates statuses for whom to remove generated certificate :param certificate_statuses: certificates statuses for whom to remove generated certificate
""" """
certificates = GeneratedCertificate.objects.filter( certificates = GeneratedCertificate.objects.filter( # pylint: disable=no-member
user__in=enrolled_students, user__in=enrolled_students,
course_id=course_id, course_id=course_id,
status__in=certificate_statuses, status__in=certificate_statuses,
......
...@@ -1802,7 +1802,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase): ...@@ -1802,7 +1802,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
}, },
result result
) )
generated_certificates = GeneratedCertificate.objects.filter( generated_certificates = GeneratedCertificate.eligible_certificates.filter(
user__in=students, user__in=students,
course_id=self.course.id, course_id=self.course.id,
mode='honor' mode='honor'
...@@ -1912,7 +1912,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase): ...@@ -1912,7 +1912,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
result result
) )
generated_certificates = GeneratedCertificate.objects.filter( generated_certificates = GeneratedCertificate.eligible_certificates.filter(
user__in=students, user__in=students,
course_id=self.course.id, course_id=self.course.id,
mode='honor' mode='honor'
......
...@@ -14,11 +14,11 @@ def _listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable ...@@ -14,11 +14,11 @@ def _listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable
Catches the signal that a course has been published in Studio and Catches the signal that a course has been published in Studio and
sets the verification deadline date to a default. sets the verification deadline date to a default.
""" """
try: course = modulestore().get_course(course_key)
deadline = VerificationDeadline.objects.get(course_key=course_key) if course:
if deadline and not deadline.deadline_is_explicit: try:
course = modulestore().get_course(course_key) deadline = VerificationDeadline.objects.get(course_key=course_key)
if course and deadline.deadline != course.end: if not deadline.deadline_is_explicit and deadline.deadline != course.end:
VerificationDeadline.set_deadline(course_key, course.end) VerificationDeadline.set_deadline(course_key, course.end)
except ObjectDoesNotExist: except ObjectDoesNotExist:
pass VerificationDeadline.set_deadline(course_key, course.end)
...@@ -24,13 +24,13 @@ class VerificationDeadlineSignalTest(ModuleStoreTestCase): ...@@ -24,13 +24,13 @@ class VerificationDeadlineSignalTest(ModuleStoreTestCase):
VerificationDeadline.objects.all().delete() VerificationDeadline.objects.all().delete()
def test_no_deadline(self): def test_no_deadline(self):
""" Verify the signal does not raise error when no deadlines found. """ """ Verify the signal sets deadline to course end when no deadline exists."""
_listen_for_course_publish('store', self.course.id) _listen_for_course_publish('store', self.course.id)
self.assertIsNone(_listen_for_course_publish('store', self.course.id)) self.assertEqual(VerificationDeadline.deadline_for_course(self.course.id), self.course.end)
def test_deadline(self): def test_deadline(self):
""" Verify deadline is set to course end date by signal. """ """ Verify deadline is set to course end date by signal when changed. """
deadline = datetime.now(tz=UTC) - timedelta(days=7) deadline = datetime.now(tz=UTC) - timedelta(days=7)
VerificationDeadline.set_deadline(self.course.id, deadline) VerificationDeadline.set_deadline(self.course.id, deadline)
......
...@@ -19,6 +19,8 @@ Common traits: ...@@ -19,6 +19,8 @@ Common traits:
import datetime import datetime
import json import json
import dateutil
from .common import * from .common import *
from openedx.core.lib.logsettings import get_logger_config from openedx.core.lib.logsettings import get_logger_config
import os import os
...@@ -732,3 +734,11 @@ JWT_EXPIRATION = ENV_TOKENS.get('JWT_EXPIRATION', JWT_EXPIRATION) ...@@ -732,3 +734,11 @@ JWT_EXPIRATION = ENV_TOKENS.get('JWT_EXPIRATION', JWT_EXPIRATION)
PROCTORING_BACKEND_PROVIDER = AUTH_TOKENS.get("PROCTORING_BACKEND_PROVIDER", PROCTORING_BACKEND_PROVIDER) PROCTORING_BACKEND_PROVIDER = AUTH_TOKENS.get("PROCTORING_BACKEND_PROVIDER", PROCTORING_BACKEND_PROVIDER)
PROCTORING_SETTINGS = ENV_TOKENS.get("PROCTORING_SETTINGS", PROCTORING_SETTINGS) PROCTORING_SETTINGS = ENV_TOKENS.get("PROCTORING_SETTINGS", PROCTORING_SETTINGS)
################# MICROSITE ####################
MICROSITE_CONFIGURATION = ENV_TOKENS.get('MICROSITE_CONFIGURATION', {})
MICROSITE_ROOT_DIR = path(ENV_TOKENS.get('MICROSITE_ROOT_DIR', ''))
# Cutoff date for granting audit certificates
if ENV_TOKENS.get('AUDIT_CERT_CUTOFF_DATE', None):
AUDIT_CERT_CUTOFF_DATE = dateutil.parser.parse(ENV_TOKENS.get('AUDIT_CERT_CUTOFF_DATE'))
...@@ -2679,3 +2679,10 @@ CCX_MAX_STUDENTS_ALLOWED = 200 ...@@ -2679,3 +2679,10 @@ CCX_MAX_STUDENTS_ALLOWED = 200
# financial assistance form # financial assistance form
FINANCIAL_ASSISTANCE_MIN_LENGTH = 800 FINANCIAL_ASSISTANCE_MIN_LENGTH = 800
FINANCIAL_ASSISTANCE_MAX_LENGTH = 2500 FINANCIAL_ASSISTANCE_MAX_LENGTH = 2500
# Deprecated xblock types
DEPRECATED_ADVANCED_COMPONENT_TYPES = []
# Cutoff date for granting audit certificates
AUDIT_CERT_CUTOFF_DATE = None
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