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):
@expiration_datetime.setter
def expiration_datetime(self, new_datetime):
""" 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
@classmethod
......@@ -590,6 +592,18 @@ class CourseMode(models.Model):
modes = cls.modes_for_course(course_id)
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):
"""
Takes a mode model and turns it into a model named tuple.
......
......@@ -421,3 +421,25 @@ class CourseModeModelTest(TestCase):
self.assertFalse(verified_mode.expiration_datetime_is_explicit)
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):
cert_grades = {
cert.user.username: cert.grade
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"
......
......@@ -296,6 +296,8 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa
CertificateStatuses.notpassing: 'notpassing',
CertificateStatuses.restricted: 'restricted',
CertificateStatuses.auditing: 'auditing',
CertificateStatuses.audit_passing: 'auditing',
CertificateStatuses.audit_notpassing: 'auditing',
}
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 @@
LOCK TABLES `django_migrations` WRITE;
/*!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 */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
......@@ -34,4 +34,4 @@ UNLOCK TABLES;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!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` (
PRIMARY KEY (`id`),
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`)
) ENGINE=InnoDB AUTO_INCREMENT=704 DEFAULT CHARSET=utf8;
) ENGINE=InnoDB AUTO_INCREMENT=698 DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `auth_registration`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
......@@ -650,6 +650,24 @@ CREATE TABLE `certificates_certificategenerationcoursesetting` (
KEY `certificates_certificategenerationcoursesetting_c8235886` (`course_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!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`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
......@@ -767,6 +785,7 @@ CREATE TABLE `certificates_generatedcertificate` (
`user_id` int(11) NOT NULL,
PRIMARY KEY (`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`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
......@@ -959,11 +978,26 @@ CREATE TABLE `course_modes_coursemode` (
`suggested_prices` varchar(255) NOT NULL,
`description` longtext,
`sku` varchar(255) DEFAULT NULL,
`expiration_datetime_is_explicit` tinyint(1) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `course_modes_coursemode_course_id_6fbb1796ace558b4_uniq` (`course_id`,`mode_slug`,`currency`),
KEY `course_modes_coursemode_ea134da7` (`course_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!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`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
......@@ -1016,6 +1050,12 @@ CREATE TABLE `course_overviews_courseoverview` (
`enrollment_domain` longtext,
`invitation_only` tinyint(1) NOT 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`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
......@@ -1445,7 +1485,7 @@ CREATE TABLE `django_content_type` (
`model` varchar(100) NOT NULL,
PRIMARY KEY (`id`),
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 */;
DROP TABLE IF EXISTS `django_migrations`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
......@@ -1456,7 +1496,7 @@ CREATE TABLE `django_migrations` (
`name` varchar(255) NOT NULL,
`applied` datetime(6) NOT NULL,
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 */;
DROP TABLE IF EXISTS `django_openid_auth_association`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
......@@ -2455,8 +2495,12 @@ CREATE TABLE `programs_programsapiconfig` (
`internal_service_url` varchar(200) NOT NULL,
`public_service_url` varchar(200) 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,
`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`),
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`)
......@@ -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`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!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`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
......@@ -3580,6 +3640,7 @@ CREATE TABLE `verify_student_historicalverificationdeadline` (
`history_date` datetime(6) NOT NULL,
`history_type` varchar(1) NOT NULL,
`history_user_id` int(11) DEFAULT NULL,
`deadline_is_explicit` tinyint(1) NOT NULL,
PRIMARY KEY (`history_id`),
KEY `verify_student__history_user_id_1e374d24cb7902c2_fk_auth_user_id` (`history_user_id`),
KEY `verify_student_historicalverificationdeadline_b80bb774` (`id`),
......@@ -3702,6 +3763,7 @@ CREATE TABLE `verify_student_verificationdeadline` (
`modified` datetime(6) NOT NULL,
`course_key` varchar(255) NOT NULL,
`deadline` datetime(6) NOT NULL,
`deadline_is_explicit` tinyint(1) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `course_key` (`course_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
......@@ -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`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!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`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
......
......@@ -77,7 +77,7 @@ def get_certificates_for_user(username):
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,
if insecure:
xqueue.use_https = False
generate_pdf = not has_html_certificates_enabled(course_key, course)
status, cert = xqueue.add_cert(student, course_key,
course=course,
generate_pdf=generate_pdf,
forced_grade=forced_grade)
if status in [CertificateStatuses.generating, CertificateStatuses.downloadable]:
cert = xqueue.add_cert(
student,
course_key,
course=course,
generate_pdf=generate_pdf,
forced_grade=forced_grade
)
if cert.status in [CertificateStatuses.generating, CertificateStatuses.downloadable]:
emit_certificate_event('created', student, course_key, course, {
'user_id': student.id,
'course_id': unicode(course_key),
......@@ -120,7 +123,7 @@ def generate_user_certificates(student, course_key, course=None, insecure=False,
'enrollment_mode': cert.mode,
'generation_mode': generation_mode
})
return status
return cert.status
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):
)
return url
try:
user_certificate = GeneratedCertificate.objects.get(
user_certificate = GeneratedCertificate.eligible_certificates.get(
user=user_id,
course_id=course_id
)
......
......@@ -76,7 +76,7 @@ class Command(BaseCommand):
status = options.get('status', CertificateStatuses.downloadable)
grade = options.get('grade', '')
cert, created = GeneratedCertificate.objects.get_or_create(
cert, created = GeneratedCertificate.eligible_certificates.get_or_create(
user=user,
course_id=course_key
)
......
......@@ -42,8 +42,9 @@ class Command(BaseCommand):
def handle(self, *args, **options):
course_id = options['course']
print "Fetching ungraded students for {0}".format(course_id)
ungraded = GeneratedCertificate.objects.filter(
course_id__exact=course_id).filter(grade__exact='')
ungraded = GeneratedCertificate.objects.filter( # pylint: disable=no-member
course_id__exact=course_id
).filter(grade__exact='')
course = courses.get_course_by_id(course_id)
factory = RequestFactory()
request = factory.get('/')
......
......@@ -70,14 +70,17 @@ class Command(BaseCommand):
enrolled_total = User.objects.filter(
courseenrollment__course_id=course_id
)
verified_enrolled = GeneratedCertificate.objects.filter(
course_id__exact=course_id, mode__exact='verified'
verified_enrolled = GeneratedCertificate.objects.filter( # pylint: disable=no-member
course_id__exact=course_id,
mode__exact='verified'
)
honor_enrolled = GeneratedCertificate.objects.filter(
course_id__exact=course_id, mode__exact='honor'
honor_enrolled = GeneratedCertificate.objects.filter( # pylint: disable=no-member
course_id__exact=course_id,
mode__exact='honor'
)
audit_enrolled = GeneratedCertificate.objects.filter(
course_id__exact=course_id, mode__exact='audit'
audit_enrolled = GeneratedCertificate.objects.filter( # pylint: disable=no-member
course_id__exact=course_id,
mode__exact='audit'
)
cert_data[course_id] = {
......@@ -88,7 +91,7 @@ class Command(BaseCommand):
'audit_enrolled': audit_enrolled.count()
}
status_tally = GeneratedCertificate.objects.filter(
status_tally = GeneratedCertificate.objects.filter( # pylint: disable=no-member
course_id__exact=course_id
).values('status').annotate(
dcount=Count('status')
......@@ -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,
status__exact='downloadable'
).values('mode').annotate(
......
......@@ -81,7 +81,7 @@ class Command(BaseCommand):
# Retrieve the IDs of generated certificates with
# error status in the set of courses we're considering.
queryset = (
GeneratedCertificate.objects.select_related('user')
GeneratedCertificate.objects.select_related('user') # pylint: disable=no-member
).filter(status=CertificateStatuses.error)
if only_course_keys:
queryset = queryset.filter(course_id__in=only_course_keys)
......
......@@ -86,6 +86,8 @@ class CertificateStatuses(object):
restricted = 'restricted'
unavailable = 'unavailable'
auditing = 'auditing'
audit_passing = 'audit_passing'
audit_notpassing = 'audit_notpassing'
readable_statuses = {
downloadable: "already received",
......@@ -143,7 +145,7 @@ class CertificateWhitelist(models.Model):
if student:
white_list = white_list.filter(user=student)
result = []
generated_certificates = GeneratedCertificate.objects.filter(
generated_certificates = GeneratedCertificate.eligible_certificates.filter(
course_id=course_id,
user__in=[exception.user for exception in white_list],
status=CertificateStatuses.downloadable
......@@ -168,11 +170,42 @@ class CertificateWhitelist(models.Model):
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):
"""
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')
VERIFIED_CERTS_MODES = [CourseMode.VERIFIED, CourseMode.CREDIT_MODE]
......@@ -348,7 +381,7 @@ def certificate_status_for_student(student, course_id):
'''
try:
generated_certificate = GeneratedCertificate.objects.get(
generated_certificate = GeneratedCertificate.objects.get( # pylint: disable=no-member
user=student, course_id=course_id)
cert_status = {
'status': generated_certificate.status,
......
......@@ -20,6 +20,7 @@ from student.models import UserProfile, CourseEnrollment
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from certificates.models import (
CertificateStatuses,
GeneratedCertificate,
certificate_status_for_student,
CertificateStatuses as status,
......@@ -120,14 +121,14 @@ class XQueueCertInterface(object):
Change the certificate status to unavailable (if it exists) and request
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
# repo also, do a deletion followed by a creation r/t a simple
# recreation. XXX: this leaves orphan cert files laying around in
# AWS. See note in the docstring too.
try:
certificate = GeneratedCertificate.objects.get(user=student, course_id=course_id)
certificate = GeneratedCertificate.eligible_certificates.get(user=student, course_id=course_id)
LOGGER.info(
(
......@@ -183,8 +184,7 @@ class XQueueCertInterface(object):
raise NotImplementedError
# pylint: disable=too-many-statements
def add_cert(self, student, course_id, course=None, forced_grade=None, template_file=None,
title='None', generate_pdf=True):
def add_cert(self, student, course_id, course=None, forced_grade=None, template_file=None, generate_pdf=True):
"""
Request a new certificate for a student.
......@@ -211,7 +211,7 @@ class XQueueCertInterface(object):
If a student does not have a passing grade the status
will change to status.notpassing
Returns the student's status and newly created certificate instance
Returns the newly created certificate instance
"""
valid_statuses = [
......@@ -220,11 +220,13 @@ class XQueueCertInterface(object):
status.deleted,
status.error,
status.notpassing,
status.downloadable
status.downloadable,
status.auditing,
status.audit_passing,
status.audit_notpassing,
]
cert_status = certificate_status_for_student(student, course_id)['status']
new_status = cert_status
cert = None
if cert_status not in valid_statuses:
......@@ -239,169 +241,193 @@ class XQueueCertInterface(object):
cert_status,
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:
# grade the student
# re-use the course passed in optionally so we don't have to re-fetch everything
# for every student
if course is None:
course = modulestore().get_course(course_id, depth=0)
profile = UserProfile.objects.get(user=student)
profile_name = profile.name
# Needed
self.request.user = student
self.request.session = {}
course_name = course.display_name or unicode(course_id)
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
# For credit mode generate verified certificate
if cert_mode == CourseMode.CREDIT_MODE:
cert_mode = CourseMode.VERIFIED
if 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:
# honor code and audit students
template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id)
if forced_grade:
grade['grade'] = forced_grade
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:
# honor code and audit students
template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id)
if forced_grade:
grade['grade'] = forced_grade
cert, created = GeneratedCertificate.objects.get_or_create(user=student, course_id=course_id) # pylint: disable=no-member
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()
passing = True
except (TypeError, XMLSyntaxError, ParserError) as exc:
LOGGER.info(
(
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,
unicode(course_id),
grade_contents,
unicode(exc)
)
# Log if the student is whitelisted
if is_whitelisted:
LOGGER.info(
(
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'"
),
u"Student %s is whitelisted in '%s'",
student.id,
unicode(course_id),
grade_contents,
unicode(exc)
unicode(course_id)
)
# 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
)
passing = True
else:
new_status = status.notpassing
cert.status = new_status
cert.save()
passing = False
# 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(
(
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."
u"The certificate status has been set to '%s'. "
u"Sent a certificate grading task to the XQueue "
u"with the key '%s'. "
),
student.id,
unicode(course_id),
new_status
cert.status,
key
)
return new_status, cert
return cert
def add_example_cert(self, example_cert):
"""Add a task to create an example certificate.
......
......@@ -228,7 +228,7 @@ class GenerateUserCertificatesTest(EventTestMixin, WebCertificateTestMixin, Modu
certs_api.generate_user_certificates(self.student, self.course.id)
# 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.assert_event_emitted(
'edx.certificate.created',
......@@ -246,7 +246,7 @@ class GenerateUserCertificatesTest(EventTestMixin, WebCertificateTestMixin, Modu
certs_api.generate_user_certificates(self.student, self.course.id)
# 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.assertIn(self.ERROR_REASON, cert.error_reason)
......@@ -260,7 +260,7 @@ class GenerateUserCertificatesTest(EventTestMixin, WebCertificateTestMixin, Modu
certs_api.generate_user_certificates(self.student, self.course.id)
# 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)
@patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': False})
......
......@@ -6,6 +6,7 @@ from nose.plugins.attrib import attr
from django.test.utils import override_settings
from mock import patch
from course_modes.models import CourseMode
from opaque_keys.edx.locator import CourseLocator
from certificates.tests.factories import BadgeAssertionFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
......@@ -30,16 +31,17 @@ class CertificateManagementTest(ModuleStoreTestCase):
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. """
# Enroll the user in the course
CourseEnrollmentFactory.create(
user=user,
course_id=course_key
course_id=course_key,
mode=mode
)
# Create the certificate
GeneratedCertificate.objects.create(
GeneratedCertificate.eligible_certificates.create(
user=user,
course_id=course_key,
status=status
......@@ -52,7 +54,7 @@ class CertificateManagementTest(ModuleStoreTestCase):
def _assert_cert_status(self, course_key, user, expected_status):
"""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)
......@@ -61,9 +63,10 @@ class CertificateManagementTest(ModuleStoreTestCase):
class ResubmitErrorCertificatesTest(CertificateManagementTest):
"""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'
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'
with check_mongo_calls(1):
......@@ -198,7 +201,7 @@ class RegenerateCertificatesTest(CertificateManagementTest):
username=self.user.email, course=unicode(key), noop=False, insecure=True, template_file=None,
grade_value=None
)
certificate = GeneratedCertificate.objects.get(
certificate = GeneratedCertificate.eligible_certificates.get(
user=self.user,
course_id=key
)
......@@ -236,7 +239,7 @@ class UngenerateCertificatesTest(CertificateManagementTest):
course=unicode(key), noop=False, insecure=True, force=False
)
self.assertTrue(mock_send_to_queue.called)
certificate = GeneratedCertificate.objects.get(
certificate = GeneratedCertificate.eligible_certificates.get(
user=self.user,
course_id=key
)
......
......@@ -28,7 +28,7 @@ class CreateFakeCertTest(TestCase):
cert_mode='verified',
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.mode, 'verified')
self.assertEqual(cert.grade, '0.89')
......@@ -41,7 +41,7 @@ class CreateFakeCertTest(TestCase):
unicode(self.COURSE_KEY),
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')
def test_too_few_args(self):
......
......@@ -8,13 +8,21 @@ from django.test.utils import override_settings
from nose.plugins.attrib import attr
from path import Path as path
from opaque_keys.edx.locator import CourseLocator
from certificates.models import (
ExampleCertificate,
ExampleCertificateSet,
CertificateHtmlViewConfiguration,
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['CERTS_HTML_VIEW_CONFIG_PATH'] = 'invalid/path/to/config.json'
......@@ -234,3 +242,42 @@ class CertificateTemplateAssetTest(TestCase):
certificate_template_asset = CertificateTemplateAsset.objects.get(id=1)
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 -*-
"""Tests for the XQueue certificates interface. """
from contextlib import contextmanager
from datetime import datetime, timedelta
import ddt
import json
from mock import patch, Mock
......@@ -8,7 +9,10 @@ from nose.plugins.attrib import attr
from django.test import TestCase
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 xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from student.tests.factories import UserFactory, CourseEnrollmentFactory
......@@ -22,13 +26,14 @@ from xmodule.modulestore.tests.factories import CourseFactory
# in our `XQueueCertInterface` implementation.
from capa.xqueue_interface import XQueueInterface
from certificates.queue import XQueueCertInterface
from certificates.models import (
ExampleCertificateSet,
ExampleCertificate,
GeneratedCertificate,
CertificateStatuses,
)
from certificates.queue import XQueueCertInterface
from certificates.tests.factories import CertificateWhitelistFactory, GeneratedCertificateFactory
from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory
......@@ -74,17 +79,22 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase):
# Verify that add_cert method does not add message to queue
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.assertIsNotNone(certificate.verify_uuid)
@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):
"""Test certificates generations for honor and audit modes."""
template_name = 'certificate-template-{id.org}-{id.course}.pdf'.format(
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')
def test_add_cert_with_verified_certificates(self, mode):
......@@ -95,10 +105,40 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase):
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):
"""Dry method for course enrollment and adding request to queue."""
def test_ineligible_cert_whitelisted(self):
"""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(
user=self.user_2,
course_id=self.course.id,
......@@ -109,19 +149,137 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase):
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)
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
self.assertTrue(mock_send.called)
__, kwargs = mock_send.call_args_list[0]
actual_header = json.loads(kwargs['header'])
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'])
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')
@override_settings(CERT_QUEUE='certificates')
......
......@@ -64,7 +64,7 @@ class CertificateSupportTestCase(TestCase):
)
# Create certificates for the student
self.cert = GeneratedCertificate.objects.create(
self.cert = GeneratedCertificate.eligible_certificates.create(
user=self.student,
course_id=self.CERT_COURSE_KEY,
grade=self.CERT_GRADE,
......@@ -244,7 +244,7 @@ class CertificateRegenerateTests(ModuleStoreTestCase, CertificateSupportTestCase
# Check that the user's certificate was updated
# Since the student hasn't actually passed the course,
# 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)
def test_regenerate_certificate_missing_params(self):
......@@ -283,7 +283,7 @@ class CertificateRegenerateTests(ModuleStoreTestCase, CertificateSupportTestCase
def test_regenerate_user_has_no_certificate(self):
# Delete the user's certificate
GeneratedCertificate.objects.all().delete()
GeneratedCertificate.eligible_certificates.all().delete()
# Should be able to regenerate
response = self._regenerate(
......@@ -293,7 +293,7 @@ class CertificateRegenerateTests(ModuleStoreTestCase, CertificateSupportTestCase
self.assertEqual(response.status_code, 200)
# 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)
def _regenerate(self, course_key=None, username=None):
......
......@@ -210,7 +210,7 @@ class MicrositeCertificatesViewsTests(ModuleStoreTestCase):
self.user.profile.name = "Joe User"
self.user.profile.save()
self.client.login(username=self.user.username, password='foo')
self.cert = GeneratedCertificate.objects.create(
self.cert = GeneratedCertificate.eligible_certificates.create(
user=self.user,
course_id=self.course_id,
download_uuid=uuid4(),
......
......@@ -13,6 +13,7 @@ from django.core.urlresolvers import reverse
from django.test.client import Client
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 student.tests.factories import UserFactory, CourseEnrollmentFactory
from student.roles import CourseStaffRole
......@@ -95,7 +96,8 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
)
CourseEnrollmentFactory.create(
user=self.user,
course_id=self.course_id
course_id=self.course_id,
mode=CourseMode.HONOR,
)
CertificateHtmlViewConfigurationFactory.create()
LinkedInAddToProfileConfigurationFactory.create()
......@@ -360,6 +362,38 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
self.assertIn("Cannot Find Certificate", 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)
def test_html_view_for_invalid_certificate(self):
"""
......@@ -515,7 +549,7 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
course_id=unicode(self.course.id)
)
self.cert.delete()
self.assertEqual(len(GeneratedCertificate.objects.all()), 0)
self.assertEqual(len(GeneratedCertificate.eligible_certificates.all()), 0)
response = self.client.get(test_url)
self.assertIn('invalid', response.content)
......@@ -538,7 +572,7 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
preview mode. Either the certificate is marked active or not.
"""
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)
test_url = get_certificate_url(
user_id=self.user.id,
......
......@@ -345,7 +345,7 @@ def _get_user_certificate(request, user, course_key, course, preview_mode=None):
else:
# certificate is being viewed by learner or public
try:
user_certificate = GeneratedCertificate.objects.get(
user_certificate = GeneratedCertificate.eligible_certificates.get(
user=user,
course_id=course_key,
status=CertificateStatuses.downloadable
......@@ -463,7 +463,7 @@ def render_cert_by_uuid(request, certificate_uuid):
This public view generates an HTML representation of the specified certificate
"""
try:
certificate = GeneratedCertificate.objects.get(
certificate = GeneratedCertificate.eligible_certificates.get(
verify_uuid=certificate_uuid,
status=CertificateStatuses.downloadable
)
......
......@@ -75,7 +75,7 @@ def update_certificate(request):
try:
course_key = SlashSeparatedCourseKey.from_deprecated_string(xqueue_body['course_id'])
cert = GeneratedCertificate.objects.get(
cert = GeneratedCertificate.eligible_certificates.get(
user__username=xqueue_body['username'],
course_id=course_key,
key=xqueue_header['lms_key'])
......
......@@ -618,7 +618,7 @@ class CertificateExceptionViewInstructorApiTest(SharedModuleStoreTestCase):
# Verify that certificate exception successfully removed from CertificateWhitelist and GeneratedCertificate
with self.assertRaises(ObjectDoesNotExist):
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
)
......
......@@ -2819,7 +2819,10 @@ def remove_certificate_exception(course_key, student):
)
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()
except ObjectDoesNotExist:
# 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):
report_run_date = datetime.date.today().strftime("%B %d, %Y")
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,
status=CertificateStatuses.downloadable
).values(*certificate_features).annotate(total_issued_certificate=Count('mode')))
......
......@@ -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 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,
course_id=course_id,
status__in=certificate_statuses,
......
......@@ -1802,7 +1802,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
},
result
)
generated_certificates = GeneratedCertificate.objects.filter(
generated_certificates = GeneratedCertificate.eligible_certificates.filter(
user__in=students,
course_id=self.course.id,
mode='honor'
......@@ -1912,7 +1912,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
result
)
generated_certificates = GeneratedCertificate.objects.filter(
generated_certificates = GeneratedCertificate.eligible_certificates.filter(
user__in=students,
course_id=self.course.id,
mode='honor'
......
......@@ -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
sets the verification deadline date to a default.
"""
try:
deadline = VerificationDeadline.objects.get(course_key=course_key)
if deadline and not deadline.deadline_is_explicit:
course = modulestore().get_course(course_key)
if course and deadline.deadline != course.end:
course = modulestore().get_course(course_key)
if course:
try:
deadline = VerificationDeadline.objects.get(course_key=course_key)
if not deadline.deadline_is_explicit and deadline.deadline != course.end:
VerificationDeadline.set_deadline(course_key, course.end)
except ObjectDoesNotExist:
pass
except ObjectDoesNotExist:
VerificationDeadline.set_deadline(course_key, course.end)
......@@ -24,13 +24,13 @@ class VerificationDeadlineSignalTest(ModuleStoreTestCase):
VerificationDeadline.objects.all().delete()
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)
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):
""" 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)
VerificationDeadline.set_deadline(self.course.id, deadline)
......
......@@ -19,6 +19,8 @@ Common traits:
import datetime
import json
import dateutil
from .common import *
from openedx.core.lib.logsettings import get_logger_config
import os
......@@ -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_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
# financial assistance form
FINANCIAL_ASSISTANCE_MIN_LENGTH = 800
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