Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
984f8732
Commit
984f8732
authored
Mar 02, 2015
by
Will Daly
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #7163 from edx/will/example-certificates
ECOM-1139: Example certificates.
parents
d147a655
9530021b
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
1442 additions
and
177 deletions
+1442
-177
lms/djangoapps/certificates/api.py
+134
-4
lms/djangoapps/certificates/migrations/0018_add_example_cert_models.py
+159
-0
lms/djangoapps/certificates/models.py
+324
-3
lms/djangoapps/certificates/queue.py
+137
-26
lms/djangoapps/certificates/tests/test_api.py
+257
-0
lms/djangoapps/certificates/tests/test_models.py
+73
-0
lms/djangoapps/certificates/tests/test_queue.py
+97
-0
lms/djangoapps/certificates/tests/test_views.py
+153
-0
lms/djangoapps/certificates/tests/tests_api.py
+0
-141
lms/djangoapps/certificates/views.py
+106
-3
lms/urls.py
+2
-0
No files found.
lms/djangoapps/certificates/api.py
View file @
984f8732
"""
Certificates API
"""
"""Certificates API
This is a Python API for generating certificates asynchronously.
Other Django apps should use the API functions defined in this module
rather than importing Django models directly.
"""
import
logging
from
certificates.models
import
CertificateStatuses
as
cert_status
,
certificate_status_for_student
from
certificates.models
import
(
CertificateStatuses
as
cert_status
,
certificate_status_for_student
,
CertificateGenerationCourseSetting
,
CertificateGenerationConfiguration
,
ExampleCertificateSet
)
from
certificates.queue
import
XQueueCertInterface
log
=
logging
.
getLogger
(
"edx.certificate"
)
...
...
@@ -63,3 +72,124 @@ def certificate_downloadable_status(student, course_key):
response_data
[
'download_url'
]
=
current_status
[
'download_url'
]
return
response_data
def
set_cert_generation_enabled
(
course_key
,
is_enabled
):
"""Enable or disable self-generated certificates for a course.
There are two "switches" that control whether self-generated certificates
are enabled for a course:
1) Whether the self-generated certificates feature is enabled.
2) Whether self-generated certificates have been enabled for this particular course.
The second flag should be enabled *only* when someone has successfully
generated example certificates for the course. This helps avoid
configuration errors (for example, not having a template configured
for the course installed on the workers). The UI for the instructor
dashboard enforces this constraint.
Arguments:
course_key (CourseKey): The course identifier.
Keyword Arguments:
is_enabled (boolean): If provided, enable/disable self-generated
certificates for this course.
"""
CertificateGenerationCourseSetting
.
set_enabled_for_course
(
course_key
,
is_enabled
)
if
is_enabled
:
log
.
info
(
u"Enabled self-generated certificates for course '
%
s'."
,
unicode
(
course_key
))
else
:
log
.
info
(
u"Disabled self-generated certificates for course '
%
s'."
,
unicode
(
course_key
))
def
cert_generation_enabled
(
course_key
):
"""Check whether certificate generation is enabled for a course.
There are two "switches" that control whether self-generated certificates
are enabled for a course:
1) Whether the self-generated certificates feature is enabled.
2) Whether self-generated certificates have been enabled for this particular course.
Certificates are enabled for a course only when both switches
are set to True.
Arguments:
course_key (CourseKey): The course identifier.
Returns:
boolean: Whether self-generated certificates are enabled
for the course.
"""
return
(
CertificateGenerationConfiguration
.
current
()
.
enabled
and
CertificateGenerationCourseSetting
.
is_enabled_for_course
(
course_key
)
)
def
generate_example_certificates
(
course_key
):
"""Generate example certificates for a course.
Example certificates are used to validate that certificates
are configured correctly for the course. Staff members can
view the example certificates before enabling
the self-generated certificates button for students.
Several example certificates may be generated for a course.
For example, if a course offers both verified and honor certificates,
examples of both types of certificate will be generated.
If an error occurs while starting the certificate generation
job, the errors will be recorded in the database and
can be retrieved using `example_certificate_status()`.
Arguments:
course_key (CourseKey): The course identifier.
Returns:
None
"""
xqueue
=
XQueueCertInterface
()
for
cert
in
ExampleCertificateSet
.
create_example_set
(
course_key
):
xqueue
.
add_example_cert
(
cert
)
log
.
info
(
u"Started generated example certificates for course '
%
s'."
,
course_key
)
def
example_certificates_status
(
course_key
):
"""Check the status of example certificates for a course.
This will check the *latest* example certificate task.
This is generally what we care about in terms of enabling/disabling
self-generated certificates for a course.
Arguments:
course_key (CourseKey): The course identifier.
Returns:
list
Example Usage:
>>> from certificates import api as certs_api
>>> certs_api.example_certificate_status(course_key)
[
{
'description': 'honor',
'status': 'success',
'download_url': 'http://www.example.com/abcd/honor_cert.pdf'
},
{
'description': 'verified',
'status': 'error',
'error_reason': 'No template found!'
}
]
"""
return
ExampleCertificateSet
.
latest_status
(
course_key
)
lms/djangoapps/certificates/migrations/0018_add_example_cert_models.py
0 → 100644
View file @
984f8732
# -*- coding: utf-8 -*-
import
datetime
from
south.db
import
db
from
south.v2
import
SchemaMigration
from
django.db
import
models
class
Migration
(
SchemaMigration
):
def
forwards
(
self
,
orm
):
# Adding model 'CertificateGenerationCourseSetting'
db
.
create_table
(
'certificates_certificategenerationcoursesetting'
,
(
(
'id'
,
self
.
gf
(
'django.db.models.fields.AutoField'
)(
primary_key
=
True
)),
(
'created'
,
self
.
gf
(
'model_utils.fields.AutoCreatedField'
)(
default
=
datetime
.
datetime
.
now
)),
(
'modified'
,
self
.
gf
(
'model_utils.fields.AutoLastModifiedField'
)(
default
=
datetime
.
datetime
.
now
)),
(
'course_key'
,
self
.
gf
(
'xmodule_django.models.CourseKeyField'
)(
max_length
=
255
,
db_index
=
True
)),
(
'enabled'
,
self
.
gf
(
'django.db.models.fields.BooleanField'
)(
default
=
False
)),
))
db
.
send_create_signal
(
'certificates'
,
[
'CertificateGenerationCourseSetting'
])
# Adding model 'ExampleCertificate'
db
.
create_table
(
'certificates_examplecertificate'
,
(
(
'id'
,
self
.
gf
(
'django.db.models.fields.AutoField'
)(
primary_key
=
True
)),
(
'created'
,
self
.
gf
(
'model_utils.fields.AutoCreatedField'
)(
default
=
datetime
.
datetime
.
now
)),
(
'modified'
,
self
.
gf
(
'model_utils.fields.AutoLastModifiedField'
)(
default
=
datetime
.
datetime
.
now
)),
(
'example_cert_set'
,
self
.
gf
(
'django.db.models.fields.related.ForeignKey'
)(
to
=
orm
[
'certificates.ExampleCertificateSet'
])),
(
'description'
,
self
.
gf
(
'django.db.models.fields.CharField'
)(
max_length
=
255
)),
(
'uuid'
,
self
.
gf
(
'django.db.models.fields.CharField'
)(
default
=
'2017ea093523484cb355fac9c3e7a22b'
,
unique
=
True
,
max_length
=
255
,
db_index
=
True
)),
(
'access_key'
,
self
.
gf
(
'django.db.models.fields.CharField'
)(
default
=
'8dc842be46484291a65b9ea34c3a8af8'
,
max_length
=
255
,
db_index
=
True
)),
(
'full_name'
,
self
.
gf
(
'django.db.models.fields.CharField'
)(
default
=
u'John Do
\xeb
'
,
max_length
=
255
)),
(
'template'
,
self
.
gf
(
'django.db.models.fields.CharField'
)(
max_length
=
255
)),
(
'status'
,
self
.
gf
(
'django.db.models.fields.CharField'
)(
default
=
'started'
,
max_length
=
255
)),
(
'error_reason'
,
self
.
gf
(
'django.db.models.fields.TextField'
)(
default
=
None
,
null
=
True
)),
(
'download_url'
,
self
.
gf
(
'django.db.models.fields.CharField'
)(
default
=
None
,
max_length
=
255
,
null
=
True
)),
))
db
.
create_index
(
'certificates_examplecertificate'
,
[
'uuid'
,
'access_key'
])
db
.
send_create_signal
(
'certificates'
,
[
'ExampleCertificate'
])
# Adding model 'ExampleCertificateSet'
db
.
create_table
(
'certificates_examplecertificateset'
,
(
(
'id'
,
self
.
gf
(
'django.db.models.fields.AutoField'
)(
primary_key
=
True
)),
(
'created'
,
self
.
gf
(
'model_utils.fields.AutoCreatedField'
)(
default
=
datetime
.
datetime
.
now
)),
(
'modified'
,
self
.
gf
(
'model_utils.fields.AutoLastModifiedField'
)(
default
=
datetime
.
datetime
.
now
)),
(
'course_key'
,
self
.
gf
(
'xmodule_django.models.CourseKeyField'
)(
max_length
=
255
,
db_index
=
True
)),
))
db
.
send_create_signal
(
'certificates'
,
[
'ExampleCertificateSet'
])
def
backwards
(
self
,
orm
):
# Deleting model 'CertificateGenerationCourseSetting'
db
.
delete_table
(
'certificates_certificategenerationcoursesetting'
)
# Deleting model 'ExampleCertificate'
db
.
delete_table
(
'certificates_examplecertificate'
)
# Deleting model 'ExampleCertificateSet'
db
.
delete_table
(
'certificates_examplecertificateset'
)
models
=
{
'auth.group'
:
{
'Meta'
:
{
'object_name'
:
'Group'
},
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'unique'
:
'True'
,
'max_length'
:
'80'
}),
'permissions'
:
(
'django.db.models.fields.related.ManyToManyField'
,
[],
{
'to'
:
"orm['auth.Permission']"
,
'symmetrical'
:
'False'
,
'blank'
:
'True'
})
},
'auth.permission'
:
{
'Meta'
:
{
'ordering'
:
"('content_type__app_label', 'content_type__model', 'codename')"
,
'unique_together'
:
"(('content_type', 'codename'),)"
,
'object_name'
:
'Permission'
},
'codename'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
}),
'content_type'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['contenttypes.ContentType']"
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'50'
})
},
'auth.user'
:
{
'Meta'
:
{
'object_name'
:
'User'
},
'date_joined'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'email'
:
(
'django.db.models.fields.EmailField'
,
[],
{
'max_length'
:
'75'
,
'blank'
:
'True'
}),
'first_name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'30'
,
'blank'
:
'True'
}),
'groups'
:
(
'django.db.models.fields.related.ManyToManyField'
,
[],
{
'to'
:
"orm['auth.Group']"
,
'symmetrical'
:
'False'
,
'blank'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'is_active'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'True'
}),
'is_staff'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'is_superuser'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'last_login'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'last_name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'30'
,
'blank'
:
'True'
}),
'password'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'128'
}),
'user_permissions'
:
(
'django.db.models.fields.related.ManyToManyField'
,
[],
{
'to'
:
"orm['auth.Permission']"
,
'symmetrical'
:
'False'
,
'blank'
:
'True'
}),
'username'
:
(
'django.db.models.fields.CharField'
,
[],
{
'unique'
:
'True'
,
'max_length'
:
'30'
})
},
'certificates.certificategenerationconfiguration'
:
{
'Meta'
:
{
'object_name'
:
'CertificateGenerationConfiguration'
},
'change_date'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'auto_now_add'
:
'True'
,
'blank'
:
'True'
}),
'changed_by'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['auth.User']"
,
'null'
:
'True'
,
'on_delete'
:
'models.PROTECT'
}),
'enabled'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
})
},
'certificates.certificategenerationcoursesetting'
:
{
'Meta'
:
{
'object_name'
:
'CertificateGenerationCourseSetting'
},
'course_key'
:
(
'xmodule_django.models.CourseKeyField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'created'
:
(
'model_utils.fields.AutoCreatedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'enabled'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'modified'
:
(
'model_utils.fields.AutoLastModifiedField'
,
[],
{
'default'
:
'datetime.datetime.now'
})
},
'certificates.certificatewhitelist'
:
{
'Meta'
:
{
'object_name'
:
'CertificateWhitelist'
},
'course_id'
:
(
'xmodule_django.models.CourseKeyField'
,
[],
{
'default'
:
'None'
,
'max_length'
:
'255'
,
'blank'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'user'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['auth.User']"
}),
'whitelist'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
})
},
'certificates.examplecertificate'
:
{
'Meta'
:
{
'object_name'
:
'ExampleCertificate'
},
'access_key'
:
(
'django.db.models.fields.CharField'
,
[],
{
'default'
:
"'616e8368b9b2458b8ef8217713275322'"
,
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'created'
:
(
'model_utils.fields.AutoCreatedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'description'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
}),
'download_url'
:
(
'django.db.models.fields.CharField'
,
[],
{
'default'
:
'None'
,
'max_length'
:
'255'
,
'null'
:
'True'
}),
'error_reason'
:
(
'django.db.models.fields.TextField'
,
[],
{
'default'
:
'None'
,
'null'
:
'True'
}),
'example_cert_set'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['certificates.ExampleCertificateSet']"
}),
'full_name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'default'
:
"u'John Do
\\
xeb'"
,
'max_length'
:
'255'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'modified'
:
(
'model_utils.fields.AutoLastModifiedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'status'
:
(
'django.db.models.fields.CharField'
,
[],
{
'default'
:
"'started'"
,
'max_length'
:
'255'
}),
'template'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
}),
'uuid'
:
(
'django.db.models.fields.CharField'
,
[],
{
'default'
:
"'f38e7389280d4776907ebf96ac728bac'"
,
'unique'
:
'True'
,
'max_length'
:
'255'
,
'db_index'
:
'True'
})
},
'certificates.examplecertificateset'
:
{
'Meta'
:
{
'object_name'
:
'ExampleCertificateSet'
},
'course_key'
:
(
'xmodule_django.models.CourseKeyField'
,
[],
{
'max_length'
:
'255'
,
'db_index'
:
'True'
}),
'created'
:
(
'model_utils.fields.AutoCreatedField'
,
[],
{
'default'
:
'datetime.datetime.now'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'modified'
:
(
'model_utils.fields.AutoLastModifiedField'
,
[],
{
'default'
:
'datetime.datetime.now'
})
},
'certificates.generatedcertificate'
:
{
'Meta'
:
{
'unique_together'
:
"(('user', 'course_id'),)"
,
'object_name'
:
'GeneratedCertificate'
},
'course_id'
:
(
'xmodule_django.models.CourseKeyField'
,
[],
{
'default'
:
'None'
,
'max_length'
:
'255'
,
'blank'
:
'True'
}),
'created_date'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'default'
:
'datetime.datetime.now'
,
'auto_now_add'
:
'True'
,
'blank'
:
'True'
}),
'distinction'
:
(
'django.db.models.fields.BooleanField'
,
[],
{
'default'
:
'False'
}),
'download_url'
:
(
'django.db.models.fields.CharField'
,
[],
{
'default'
:
"''"
,
'max_length'
:
'128'
,
'blank'
:
'True'
}),
'download_uuid'
:
(
'django.db.models.fields.CharField'
,
[],
{
'default'
:
"''"
,
'max_length'
:
'32'
,
'blank'
:
'True'
}),
'error_reason'
:
(
'django.db.models.fields.CharField'
,
[],
{
'default'
:
"''"
,
'max_length'
:
'512'
,
'blank'
:
'True'
}),
'grade'
:
(
'django.db.models.fields.CharField'
,
[],
{
'default'
:
"''"
,
'max_length'
:
'5'
,
'blank'
:
'True'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'key'
:
(
'django.db.models.fields.CharField'
,
[],
{
'default'
:
"''"
,
'max_length'
:
'32'
,
'blank'
:
'True'
}),
'mode'
:
(
'django.db.models.fields.CharField'
,
[],
{
'default'
:
"'honor'"
,
'max_length'
:
'32'
}),
'modified_date'
:
(
'django.db.models.fields.DateTimeField'
,
[],
{
'default'
:
'datetime.datetime.now'
,
'auto_now'
:
'True'
,
'blank'
:
'True'
}),
'name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'255'
,
'blank'
:
'True'
}),
'status'
:
(
'django.db.models.fields.CharField'
,
[],
{
'default'
:
"'unavailable'"
,
'max_length'
:
'32'
}),
'user'
:
(
'django.db.models.fields.related.ForeignKey'
,
[],
{
'to'
:
"orm['auth.User']"
}),
'verify_uuid'
:
(
'django.db.models.fields.CharField'
,
[],
{
'default'
:
"''"
,
'max_length'
:
'32'
,
'blank'
:
'True'
})
},
'contenttypes.contenttype'
:
{
'Meta'
:
{
'ordering'
:
"('name',)"
,
'unique_together'
:
"(('app_label', 'model'),)"
,
'object_name'
:
'ContentType'
,
'db_table'
:
"'django_content_type'"
},
'app_label'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
}),
'id'
:
(
'django.db.models.fields.AutoField'
,
[],
{
'primary_key'
:
'True'
}),
'model'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
}),
'name'
:
(
'django.db.models.fields.CharField'
,
[],
{
'max_length'
:
'100'
})
}
}
complete_apps
=
[
'certificates'
]
lms/djangoapps/certificates/models.py
View file @
984f8732
# -*- coding: utf-8 -*-
"""
Certificates are created for a student and an offering of a course.
...
...
@@ -44,17 +45,21 @@ Eligibility:
then the student will be issued a certificate regardless of his grade,
unless he has allow_certificate set to False.
"""
from
datetime
import
datetime
import
uuid
from
django.contrib.auth.models
import
User
from
django.db
import
models
from
django.db
import
models
,
transaction
from
django.db.models.signals
import
post_save
from
django.dispatch
import
receiver
from
django.conf
import
settings
from
d
atetime
import
datetime
from
d
jango.utils.translation
import
ugettext_lazy
from
model_utils
import
Choices
from
model_utils.models
import
TimeStampedModel
from
config_models.models
import
ConfigurationModel
from
xmodule_django.models
import
CourseKeyField
,
NoneToEmptyManager
from
util.milestones_helpers
import
fulfill_course_milestone
from
course_modes.models
import
CourseMode
class
CertificateStatuses
(
object
):
...
...
@@ -179,6 +184,322 @@ def certificate_status_for_student(student, course_id):
return
{
'status'
:
CertificateStatuses
.
unavailable
,
'mode'
:
GeneratedCertificate
.
MODES
.
honor
}
class
ExampleCertificateSet
(
TimeStampedModel
):
"""A set of example certificates.
Example certificates are used to verify that certificate
generation is working for a particular course.
A particular course may have several kinds of certificates
(e.g. honor and verified), in which case we generate
multiple example certificates for the course.
"""
course_key
=
CourseKeyField
(
max_length
=
255
,
db_index
=
True
)
class
Meta
:
# pylint: disable=missing-docstring, old-style-class
get_latest_by
=
'created'
@classmethod
@transaction.commit_on_success
def
create_example_set
(
cls
,
course_key
):
"""Create a set of example certificates for a course.
Arguments:
course_key (CourseKey)
Returns:
ExampleCertificateSet
"""
cert_set
=
cls
.
objects
.
create
(
course_key
=
course_key
)
ExampleCertificate
.
objects
.
bulk_create
([
ExampleCertificate
(
example_cert_set
=
cert_set
,
description
=
mode
.
slug
,
template
=
cls
.
_template_for_mode
(
mode
.
slug
,
course_key
)
)
for
mode
in
CourseMode
.
modes_for_course
(
course_key
)
])
return
cert_set
@classmethod
def
latest_status
(
cls
,
course_key
):
"""Summarize the latest status of example certificates for a course.
Arguments:
course_key (CourseKey)
Returns:
list: List of status dictionaries. If no example certificates
have been started yet, returns None.
"""
try
:
latest
=
cls
.
objects
.
filter
(
course_key
=
course_key
)
.
latest
()
except
cls
.
DoesNotExist
:
return
None
queryset
=
ExampleCertificate
.
objects
.
filter
(
example_cert_set
=
latest
)
.
order_by
(
'-created'
)
return
[
cert
.
status_dict
for
cert
in
queryset
]
def
__iter__
(
self
):
"""Iterate through example certificates in the set.
Yields:
ExampleCertificate
"""
queryset
=
(
ExampleCertificate
.
objects
)
.
select_related
(
'example_cert_set'
)
.
filter
(
example_cert_set
=
self
)
for
cert
in
queryset
:
yield
cert
@staticmethod
def
_template_for_mode
(
mode_slug
,
course_key
):
"""Calculate the template PDF based on the course mode. """
return
(
u"certificate-template-{key.org}-{key.course}-verified.pdf"
.
format
(
key
=
course_key
)
if
mode_slug
==
'verified'
else
u"certificate-template-{key.org}-{key.course}.pdf"
.
format
(
key
=
course_key
)
)
def
_make_uuid
():
"""Return a 32-character UUID. """
return
uuid
.
uuid4
()
.
hex
class
ExampleCertificate
(
TimeStampedModel
):
"""Example certificate.
Example certificates are used to verify that certificate
generation is working for a particular course.
An example certificate is similar to an ordinary certificate,
except that:
1) Example certificates are not associated with a particular user,
and are never displayed to students.
2) We store the "inputs" for generating the example certificate
to make it easier to debug when certificate generation fails.
3) We use dummy values.
"""
# Statuses
STATUS_STARTED
=
'started'
STATUS_SUCCESS
=
'success'
STATUS_ERROR
=
'error'
# Dummy full name for the generated certificate
EXAMPLE_FULL_NAME
=
u'John Doë'
example_cert_set
=
models
.
ForeignKey
(
ExampleCertificateSet
)
description
=
models
.
CharField
(
max_length
=
255
,
help_text
=
ugettext_lazy
(
u"A human-readable description of the example certificate. "
u"For example, 'verified' or 'honor' to differentiate between "
u"two types of certificates."
)
)
# Inputs to certificate generation
# We store this for auditing purposes if certificate
# generation fails.
uuid
=
models
.
CharField
(
max_length
=
255
,
default
=
_make_uuid
,
db_index
=
True
,
unique
=
True
,
help_text
=
ugettext_lazy
(
u"A unique identifier for the example certificate. "
u"This is used when we receive a response from the queue "
u"to determine which example certificate was processed."
)
)
access_key
=
models
.
CharField
(
max_length
=
255
,
default
=
_make_uuid
,
db_index
=
True
,
help_text
=
ugettext_lazy
(
u"An access key for the example certificate. "
u"This is used when we receive a response from the queue "
u"to validate that the sender is the same entity we asked "
u"to generate the certificate."
)
)
full_name
=
models
.
CharField
(
max_length
=
255
,
default
=
EXAMPLE_FULL_NAME
,
help_text
=
ugettext_lazy
(
u"The full name that will appear on the certificate."
)
)
template
=
models
.
CharField
(
max_length
=
255
,
help_text
=
ugettext_lazy
(
u"The template file to use when generating the certificate."
)
)
# Outputs from certificate generation
status
=
models
.
CharField
(
max_length
=
255
,
default
=
STATUS_STARTED
,
choices
=
(
(
STATUS_STARTED
,
'Started'
),
(
STATUS_SUCCESS
,
'Success'
),
(
STATUS_ERROR
,
'Error'
)
),
help_text
=
ugettext_lazy
(
u"The status of the example certificate."
)
)
error_reason
=
models
.
TextField
(
null
=
True
,
default
=
None
,
help_text
=
ugettext_lazy
(
u"The reason an error occurred during certificate generation."
)
)
download_url
=
models
.
CharField
(
max_length
=
255
,
null
=
True
,
default
=
None
,
help_text
=
ugettext_lazy
(
u"The download URL for the generated certificate."
)
)
def
update_status
(
self
,
status
,
error_reason
=
None
,
download_url
=
None
):
"""Update the status of the example certificate.
This will usually be called either:
1) When an error occurs adding the certificate to the queue.
2) When we receieve a response from the queue (either error or success).
If an error occurs, we store the error message;
if certificate generation is successful, we store the URL
for the generated certificate.
Arguments:
status (str): Either `STATUS_SUCCESS` or `STATUS_ERROR`
Keyword Arguments:
error_reason (unicode): A description of the error that occurred.
download_url (unicode): The URL for the generated certificate.
Raises:
ValueError: The status is not a valid value.
"""
if
status
not
in
[
self
.
STATUS_SUCCESS
,
self
.
STATUS_ERROR
]:
msg
=
u"Invalid status: must be either '{success}' or '{error}'."
.
format
(
success
=
self
.
STATUS_SUCCESS
,
error
=
self
.
STATUS_ERROR
)
raise
ValueError
(
msg
)
self
.
status
=
status
if
status
==
self
.
STATUS_ERROR
and
error_reason
:
self
.
error_reason
=
error_reason
if
status
==
self
.
STATUS_SUCCESS
and
download_url
:
self
.
download_url
=
download_url
self
.
save
()
@property
def
status_dict
(
self
):
"""Summarize the status of the example certificate.
Returns:
dict
"""
result
=
{
'description'
:
self
.
description
,
'status'
:
self
.
status
,
}
if
self
.
error_reason
:
result
[
'error_reason'
]
=
self
.
error_reason
if
self
.
download_url
:
result
[
'download_url'
]
=
self
.
download_url
return
result
@property
def
course_key
(
self
):
"""The course key associated with the example certificate. """
return
self
.
example_cert_set
.
course_key
class
CertificateGenerationCourseSetting
(
TimeStampedModel
):
"""Enable or disable certificate generation for a particular course.
This controls whether students are allowed to "self-generate"
certificates for a course. It does NOT prevent us from
batch-generating certificates for a course using management
commands.
In general, we should only enable self-generated certificates
for a course once we successfully generate example certificates
for the course. This is enforced in the UI layer, but
not in the data layer.
"""
course_key
=
CourseKeyField
(
max_length
=
255
,
db_index
=
True
)
enabled
=
models
.
BooleanField
(
default
=
False
)
class
Meta
:
# pylint: disable=missing-docstring, old-style-class
get_latest_by
=
'created'
@classmethod
def
is_enabled_for_course
(
cls
,
course_key
):
"""Check whether self-generated certificates are enabled for a course.
Arguments:
course_key (CourseKey): The identifier for the course.
Returns:
boolean
"""
try
:
latest
=
cls
.
objects
.
filter
(
course_key
=
course_key
)
.
latest
()
except
cls
.
DoesNotExist
:
return
False
else
:
return
latest
.
enabled
@classmethod
def
set_enabled_for_course
(
cls
,
course_key
,
is_enabled
):
"""Enable or disable self-generated certificates for a course.
Arguments:
course_key (CourseKey): The identifier for the course.
is_enabled (boolean): Whether to enable or disable self-generated certificates.
"""
CertificateGenerationCourseSetting
.
objects
.
create
(
course_key
=
course_key
,
enabled
=
is_enabled
)
class
CertificateGenerationConfiguration
(
ConfigurationModel
):
"""Configure certificate generation."""
"""Configure certificate generation.
Enable or disable the self-generated certificates feature.
When this flag is disabled, the "generate certificate" button
will be hidden on the progress page.
When the feature is enabled, the "generate certificate" button
will appear for courses that have enabled self-generated
certificates.
"""
pass
lms/djangoapps/certificates/queue.py
View file @
984f8732
from
certificates.models
import
GeneratedCertificate
from
certificates.models
import
certificate_status_for_student
from
certificates.models
import
CertificateStatuses
as
status
from
certificates.models
import
CertificateWhitelist
"""Interface for adding certificate generation tasks to the XQueue. """
import
json
import
random
import
logging
import
lxml.html
from
lxml.etree
import
XMLSyntaxError
,
ParserError
# pylint:disable=no-name-in-module
from
courseware
import
grades
,
courses
from
django.test.client
import
RequestFactory
from
capa.xqueue_interface
import
XQueueInterface
from
capa.xqueue_interface
import
make_xheader
,
make_hashkey
from
django.conf
import
settings
from
django.core.urlresolvers
import
reverse
from
requests.auth
import
HTTPBasicAuth
from
courseware
import
grades
from
xmodule.modulestore.django
import
modulestore
from
capa.xqueue_interface
import
XQueueInterface
from
capa.xqueue_interface
import
make_xheader
,
make_hashkey
from
student.models
import
UserProfile
,
CourseEnrollment
from
verify_student.models
import
SoftwareSecurePhotoVerification
import
json
import
random
import
logging
import
lxml.html
from
lxml.etree
import
XMLSyntaxError
,
ParserError
from
certificates.models
import
(
GeneratedCertificate
,
certificate_status_for_student
,
CertificateStatuses
as
status
,
CertificateWhitelist
,
ExampleCertificate
)
LOGGER
=
logging
.
getLogger
(
__name__
)
class
XQueueAddToQueueError
(
Exception
):
"""An error occurred when adding a certificate task to the queue. """
def
__init__
(
self
,
error_code
,
error_msg
):
self
.
error_code
=
error_code
self
.
error_msg
=
error_msg
super
(
XQueueAddToQueueError
,
self
)
.
__init__
(
unicode
(
self
))
def
__unicode__
(
self
):
return
(
u"Could not add certificate to the XQueue. "
u"The error code was '{code}' and the message was '{msg}'."
)
.
format
(
code
=
self
.
error_code
,
msg
=
self
.
error_msg
)
class
XQueueCertInterface
(
object
):
"""
XQueueCertificateInterface provides an
...
...
@@ -205,7 +230,7 @@ class XQueueCertInterface(object):
# re-use the course passed in optionally so we don't have to re-fetch everything
# for every student
if
course
is
None
:
course
=
courses
.
get_course_by_id
(
course_id
)
course
=
modulestore
()
.
get_course
(
course_id
,
depth
=
0
)
profile
=
UserProfile
.
objects
.
get
(
user
=
student
)
profile_name
=
profile
.
name
...
...
@@ -213,7 +238,7 @@ class XQueueCertInterface(object):
self
.
request
.
user
=
student
self
.
request
.
session
=
{}
course_name
=
course
.
display_name
or
course_id
.
to_deprecated_string
(
)
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
)
...
...
@@ -297,7 +322,7 @@ class XQueueCertInterface(object):
contents
=
{
'action'
:
'create'
,
'username'
:
student
.
username
,
'course_id'
:
course_id
.
to_deprecated_string
(
),
'course_id'
:
unicode
(
course_id
),
'course_name'
:
course_name
,
'name'
:
profile_name
,
'grade'
:
grade_contents
,
...
...
@@ -337,20 +362,106 @@ class XQueueCertInterface(object):
return
new_status
def
_send_to_xqueue
(
self
,
contents
,
key
):
"""
Create a new task on the XQueue. """
def
add_example_cert
(
self
,
example_cert
):
"""
Add a task to create an example certificate.
if
self
.
use_https
:
proto
=
"https"
else
:
proto
=
"http"
Unlike other certificates, an example certificate is
not associated with any particular user and is never
shown to students.
If an error occurs when adding the example certificate
to the queue, the example certificate status
will be set to "error".
Arguments:
example_cert (ExampleCertificate)
"""
contents
=
{
'action'
:
'create'
,
'course_id'
:
unicode
(
example_cert
.
course_key
),
'name'
:
example_cert
.
full_name
,
'template_pdf'
:
example_cert
.
template
,
# Example certificates are not associated with a particular user.
# However, we still need to find the example certificate when
# we receive a response from the queue. For this reason,
# we use the example certificate's unique identifier as a username.
# Note that the username is *not* displayed on the certificate;
# it is used only to identify the certificate task in the queue.
'username'
:
example_cert
.
uuid
,
# We send this extra parameter to differentiate
# example certificates from other certificates.
# This is not used by the certificates workers or XQueue.
'example_certificate'
:
True
,
}
# The callback for example certificates is different than the callback
# for other certificates. Although both tasks use the same queue,
# we can distinguish whether the certificate was an example cert based
# on which end-point XQueue uses once the task completes.
callback_url_path
=
reverse
(
'certificates.views.update_example_certificate'
)
try
:
self
.
_send_to_xqueue
(
contents
,
example_cert
.
access_key
,
task_identifier
=
example_cert
.
uuid
,
callback_url_path
=
callback_url_path
)
except
XQueueAddToQueueError
as
exc
:
example_cert
.
update_status
(
ExampleCertificate
.
STATUS_ERROR
,
error_reason
=
unicode
(
exc
)
)
def
_send_to_xqueue
(
self
,
contents
,
key
,
task_identifier
=
None
,
callback_url_path
=
'update_certificate'
):
"""Create a new task on the XQueue.
Arguments:
contents (dict): The contents of the XQueue task.
key (str): An access key for the task. This will be sent
to the callback end-point once the task completes,
so that we can validate that the sender is the same
entity that received the task.
Keyword Arguments:
callback_url_path (str): The path of the callback URL.
If not provided, use the default end-point for student-generated
certificates.
"""
callback_url
=
u'{protocol}://{base_url}{path}'
.
format
(
protocol
=
(
"https"
if
self
.
use_https
else
"http"
),
base_url
=
settings
.
SITE_NAME
,
path
=
callback_url_path
)
# Append the key to the URL
# This is necessary because XQueue assumes that only one
# submission is active for a particular URL.
# If it receives a second submission with the same callback URL,
# it "retires" any other submission with the same URL.
# This was a hack that depended on the URL containing the user ID
# and courseware location; an assumption that does not apply
# to certificate generation.
# XQueue also truncates the callback URL to 128 characters,
# but since our key lengths are shorter than that, this should
# not affect us.
callback_url
+=
"?key={key}"
.
format
(
key
=
(
task_identifier
if
task_identifier
is
not
None
else
key
)
)
xheader
=
make_xheader
(
'{0}://{1}/update_certificate?{2}'
.
format
(
proto
,
settings
.
SITE_NAME
,
key
),
key
,
settings
.
CERT_QUEUE
)
xheader
=
make_xheader
(
callback_url
,
key
,
settings
.
CERT_QUEUE
)
(
error
,
msg
)
=
self
.
xqueue_interface
.
send_to_queue
(
header
=
xheader
,
body
=
json
.
dumps
(
contents
))
if
error
:
LOGGER
.
critical
(
u'Unable to add a request to the queue:
%
s
%
s'
,
unicode
(
error
),
msg
)
raise
Exception
(
'Unable to send queue message'
)
exc
=
XQueueAddToQueueError
(
error
,
msg
)
LOGGER
.
critical
(
unicode
(
exc
))
raise
exc
lms/djangoapps/certificates/tests/test_api.py
0 → 100644
View file @
984f8732
"""Tests for the certificates Python API. """
from
contextlib
import
contextmanager
import
ddt
from
django.test
import
TestCase
,
RequestFactory
from
django.test.utils
import
override_settings
from
mock
import
patch
,
Mock
from
opaque_keys.edx.locator
import
CourseLocator
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
student.models
import
CourseEnrollment
from
student.tests.factories
import
UserFactory
from
course_modes.tests.factories
import
CourseModeFactory
from
config_models.models
import
cache
from
certificates
import
api
as
certs_api
from
certificates.models
import
(
CertificateStatuses
,
CertificateGenerationConfiguration
,
ExampleCertificate
)
from
certificates.queue
import
XQueueCertInterface
from
certificates.tests.factories
import
GeneratedCertificateFactory
class
CertificateDownloadableStatusTests
(
ModuleStoreTestCase
):
"""Tests for the `certificate_downloadable_status` helper function. """
def
setUp
(
self
):
super
(
CertificateDownloadableStatusTests
,
self
)
.
setUp
()
self
.
student
=
UserFactory
()
self
.
student_no_cert
=
UserFactory
()
self
.
course
=
CourseFactory
.
create
(
org
=
'edx'
,
number
=
'verified'
,
display_name
=
'Verified Course'
)
self
.
request_factory
=
RequestFactory
()
def
test_user_cert_status_with_generating
(
self
):
GeneratedCertificateFactory
.
create
(
user
=
self
.
student
,
course_id
=
self
.
course
.
id
,
status
=
CertificateStatuses
.
generating
,
mode
=
'verified'
)
self
.
assertEqual
(
certs_api
.
certificate_downloadable_status
(
self
.
student
,
self
.
course
.
id
),
{
'is_downloadable'
:
False
,
'is_generating'
:
True
,
'download_url'
:
None
}
)
def
test_user_cert_status_with_error
(
self
):
GeneratedCertificateFactory
.
create
(
user
=
self
.
student
,
course_id
=
self
.
course
.
id
,
status
=
CertificateStatuses
.
error
,
mode
=
'verified'
)
self
.
assertEqual
(
certs_api
.
certificate_downloadable_status
(
self
.
student
,
self
.
course
.
id
),
{
'is_downloadable'
:
False
,
'is_generating'
:
True
,
'download_url'
:
None
}
)
def
test_user_with_out_cert
(
self
):
self
.
assertEqual
(
certs_api
.
certificate_downloadable_status
(
self
.
student_no_cert
,
self
.
course
.
id
),
{
'is_downloadable'
:
False
,
'is_generating'
:
False
,
'download_url'
:
None
}
)
def
test_user_with_downloadable_cert
(
self
):
GeneratedCertificateFactory
.
create
(
user
=
self
.
student
,
course_id
=
self
.
course
.
id
,
status
=
CertificateStatuses
.
downloadable
,
mode
=
'verified'
,
download_url
=
'www.google.com'
)
self
.
assertEqual
(
certs_api
.
certificate_downloadable_status
(
self
.
student
,
self
.
course
.
id
),
{
'is_downloadable'
:
True
,
'is_generating'
:
False
,
'download_url'
:
'www.google.com'
}
)
class
GenerateUserCertificatesTest
(
ModuleStoreTestCase
):
"""Tests for the `generate_user_certificates` helper function. """
def
setUp
(
self
):
super
(
GenerateUserCertificatesTest
,
self
)
.
setUp
()
self
.
student
=
UserFactory
()
self
.
student_no_cert
=
UserFactory
()
self
.
course
=
CourseFactory
.
create
(
org
=
'edx'
,
number
=
'verified'
,
display_name
=
'Verified Course'
,
grade_cutoffs
=
{
'cutoff'
:
0.75
,
'Pass'
:
0.5
}
)
self
.
enrollment
=
CourseEnrollment
.
enroll
(
self
.
student
,
self
.
course
.
id
,
mode
=
'honor'
)
self
.
request_factory
=
RequestFactory
()
@override_settings
(
CERT_QUEUE
=
'certificates'
)
@patch
(
'courseware.grades.grade'
,
Mock
(
return_value
=
{
'grade'
:
'Pass'
,
'percent'
:
0.75
}))
def
test_new_cert_requests_into_xqueue_returns_generating
(
self
):
# Mock `grade.grade` and return a summary with passing score.
# New requests save into xqueue and return the status
with
patch
(
'capa.xqueue_interface.XQueueInterface.send_to_queue'
)
as
mock_send_to_queue
:
mock_send_to_queue
.
return_value
=
(
0
,
"Successfully queued"
)
result
=
certs_api
.
generate_user_certificates
(
self
.
student
,
self
.
course
)
self
.
assertEqual
(
result
,
'generating'
)
@ddt.ddt
class
CertificateGenerationEnabledTest
(
TestCase
):
"""Test enabling/disabling self-generated certificates for a course. """
COURSE_KEY
=
CourseLocator
(
org
=
'test'
,
course
=
'test'
,
run
=
'test'
)
def
setUp
(
self
):
super
(
CertificateGenerationEnabledTest
,
self
)
.
setUp
()
# Since model-based configuration is cached, we need
# to clear the cache before each test.
cache
.
clear
()
@ddt.data
(
(
None
,
None
,
False
),
(
False
,
None
,
False
),
(
False
,
True
,
False
),
(
True
,
None
,
False
),
(
True
,
False
,
False
),
(
True
,
True
,
True
)
)
@ddt.unpack
def
test_cert_generation_enabled
(
self
,
is_feature_enabled
,
is_course_enabled
,
expect_enabled
):
if
is_feature_enabled
is
not
None
:
CertificateGenerationConfiguration
.
objects
.
create
(
enabled
=
is_feature_enabled
)
if
is_course_enabled
is
not
None
:
certs_api
.
set_cert_generation_enabled
(
self
.
COURSE_KEY
,
is_course_enabled
)
self
.
_assert_enabled_for_course
(
self
.
COURSE_KEY
,
expect_enabled
)
def
test_latest_setting_used
(
self
):
# Enable the feature
CertificateGenerationConfiguration
.
objects
.
create
(
enabled
=
True
)
# Enable for the course
certs_api
.
set_cert_generation_enabled
(
self
.
COURSE_KEY
,
True
)
self
.
_assert_enabled_for_course
(
self
.
COURSE_KEY
,
True
)
# Disable for the course
certs_api
.
set_cert_generation_enabled
(
self
.
COURSE_KEY
,
False
)
self
.
_assert_enabled_for_course
(
self
.
COURSE_KEY
,
False
)
def
test_setting_is_course_specific
(
self
):
# Enable the feature
CertificateGenerationConfiguration
.
objects
.
create
(
enabled
=
True
)
# Enable for one course
certs_api
.
set_cert_generation_enabled
(
self
.
COURSE_KEY
,
True
)
self
.
_assert_enabled_for_course
(
self
.
COURSE_KEY
,
True
)
# Should be disabled for another course
other_course
=
CourseLocator
(
org
=
'other'
,
course
=
'other'
,
run
=
'other'
)
self
.
_assert_enabled_for_course
(
other_course
,
False
)
def
_assert_enabled_for_course
(
self
,
course_key
,
expect_enabled
):
"""Check that self-generated certificates are enabled or disabled for the course. """
actual_enabled
=
certs_api
.
cert_generation_enabled
(
course_key
)
self
.
assertEqual
(
expect_enabled
,
actual_enabled
)
class
GenerateExampleCertificatesTest
(
TestCase
):
"""Test generation of example certificates. """
COURSE_KEY
=
CourseLocator
(
org
=
'test'
,
course
=
'test'
,
run
=
'test'
)
def
setUp
(
self
):
super
(
GenerateExampleCertificatesTest
,
self
)
.
setUp
()
def
test_generate_example_certs
(
self
):
# Generate certificates for the course
with
self
.
_mock_xqueue
()
as
mock_queue
:
certs_api
.
generate_example_certificates
(
self
.
COURSE_KEY
)
# Verify that the appropriate certs were added to the queue
self
.
_assert_certs_in_queue
(
mock_queue
,
1
)
# Verify that the certificate status is "started"
self
.
_assert_cert_status
({
'description'
:
'honor'
,
'status'
:
'started'
})
def
test_generate_example_certs_with_verified_mode
(
self
):
# Create verified and honor modes for the course
CourseModeFactory
(
course_id
=
self
.
COURSE_KEY
,
mode_slug
=
'honor'
)
CourseModeFactory
(
course_id
=
self
.
COURSE_KEY
,
mode_slug
=
'verified'
)
# Generate certificates for the course
with
self
.
_mock_xqueue
()
as
mock_queue
:
certs_api
.
generate_example_certificates
(
self
.
COURSE_KEY
)
# Verify that the appropriate certs were added to the queue
self
.
_assert_certs_in_queue
(
mock_queue
,
2
)
# Verify that the certificate status is "started"
self
.
_assert_cert_status
(
{
'description'
:
'verified'
,
'status'
:
'started'
},
{
'description'
:
'honor'
,
'status'
:
'started'
}
)
@contextmanager
def
_mock_xqueue
(
self
):
"""Mock the XQueue method for adding a task to the queue. """
with
patch
.
object
(
XQueueCertInterface
,
'add_example_cert'
)
as
mock_queue
:
yield
mock_queue
def
_assert_certs_in_queue
(
self
,
mock_queue
,
expected_num
):
"""Check that the certificate generation task was added to the queue. """
certs_in_queue
=
[
call_args
[
0
]
for
(
call_args
,
__
)
in
mock_queue
.
call_args_list
]
self
.
assertEqual
(
len
(
certs_in_queue
),
expected_num
)
for
cert
in
certs_in_queue
:
self
.
assertTrue
(
isinstance
(
cert
,
ExampleCertificate
))
def
_assert_cert_status
(
self
,
*
expected_statuses
):
"""Check the example certificate status. """
actual_status
=
certs_api
.
example_certificates_status
(
self
.
COURSE_KEY
)
self
.
assertEqual
(
list
(
expected_statuses
),
actual_status
)
lms/djangoapps/certificates/tests/test_models.py
0 → 100644
View file @
984f8732
"""Tests for certificate Django models. """
from
django.test
import
TestCase
from
opaque_keys.edx.locator
import
CourseLocator
from
certificates.models
import
(
ExampleCertificate
,
ExampleCertificateSet
)
class
ExampleCertificateTest
(
TestCase
):
"""Tests for the ExampleCertificate model. """
COURSE_KEY
=
CourseLocator
(
org
=
'test'
,
course
=
'test'
,
run
=
'test'
)
DESCRIPTION
=
'test'
TEMPLATE
=
'test.pdf'
DOWNLOAD_URL
=
'http://www.example.com'
ERROR_REASON
=
'Kaboom!'
def
setUp
(
self
):
super
(
ExampleCertificateTest
,
self
)
.
setUp
()
self
.
cert_set
=
ExampleCertificateSet
.
objects
.
create
(
course_key
=
self
.
COURSE_KEY
)
self
.
cert
=
ExampleCertificate
.
objects
.
create
(
example_cert_set
=
self
.
cert_set
,
description
=
self
.
DESCRIPTION
,
template
=
self
.
TEMPLATE
)
def
test_update_status_success
(
self
):
self
.
cert
.
update_status
(
ExampleCertificate
.
STATUS_SUCCESS
,
download_url
=
self
.
DOWNLOAD_URL
)
self
.
assertEqual
(
self
.
cert
.
status_dict
,
{
'description'
:
self
.
DESCRIPTION
,
'status'
:
ExampleCertificate
.
STATUS_SUCCESS
,
'download_url'
:
self
.
DOWNLOAD_URL
}
)
def
test_update_status_error
(
self
):
self
.
cert
.
update_status
(
ExampleCertificate
.
STATUS_ERROR
,
error_reason
=
self
.
ERROR_REASON
)
self
.
assertEqual
(
self
.
cert
.
status_dict
,
{
'description'
:
self
.
DESCRIPTION
,
'status'
:
ExampleCertificate
.
STATUS_ERROR
,
'error_reason'
:
self
.
ERROR_REASON
}
)
def
test_update_status_invalid
(
self
):
with
self
.
assertRaisesRegexp
(
ValueError
,
'status'
):
self
.
cert
.
update_status
(
'invalid'
)
def
test_latest_status_unavailable
(
self
):
# Delete any existing statuses
ExampleCertificateSet
.
objects
.
all
()
.
delete
()
# Verify that the "latest" status is None
result
=
ExampleCertificateSet
.
latest_status
(
self
.
COURSE_KEY
)
self
.
assertIs
(
result
,
None
)
def
test_latest_status_is_course_specific
(
self
):
other_course
=
CourseLocator
(
org
=
'other'
,
course
=
'other'
,
run
=
'other'
)
result
=
ExampleCertificateSet
.
latest_status
(
other_course
)
self
.
assertIs
(
result
,
None
)
lms/djangoapps/certificates/tests/test_queue.py
0 → 100644
View file @
984f8732
# -*- coding: utf-8 -*-
"""Tests for the XQueue certificates interface. """
from
contextlib
import
contextmanager
import
json
from
mock
import
patch
from
django.test
import
TestCase
from
django.test.utils
import
override_settings
from
opaque_keys.edx.locator
import
CourseLocator
# It is really unfortunate that we are using the XQueue client
# code from the capa library. In the future, we should move this
# into a shared library. We import it here so we can mock it
# and verify that items are being correctly added to the queue
# in our `XQueueCertInterface` implementation.
from
capa.xqueue_interface
import
XQueueInterface
from
certificates.queue
import
XQueueCertInterface
from
certificates.models
import
ExampleCertificateSet
,
ExampleCertificate
@override_settings
(
CERT_QUEUE
=
'certificates'
)
class
XQueueCertInterfaceTest
(
TestCase
):
"""Tests for the XQueue interface for certificate generation. """
COURSE_KEY
=
CourseLocator
(
org
=
'test'
,
course
=
'test'
,
run
=
'test'
)
TEMPLATE
=
'test.pdf'
DESCRIPTION
=
'test'
ERROR_MSG
=
'Kaboom!'
def
setUp
(
self
):
super
(
XQueueCertInterfaceTest
,
self
)
.
setUp
()
self
.
xqueue
=
XQueueCertInterface
()
def
test_add_example_cert
(
self
):
cert
=
self
.
_create_example_cert
()
with
self
.
_mock_xqueue
()
as
mock_send
:
self
.
xqueue
.
add_example_cert
(
cert
)
# Verify that the correct payload was sent to the XQueue
self
.
_assert_queue_task
(
mock_send
,
cert
)
# Verify the certificate status
self
.
assertEqual
(
cert
.
status
,
ExampleCertificate
.
STATUS_STARTED
)
def
test_add_example_cert_error
(
self
):
cert
=
self
.
_create_example_cert
()
with
self
.
_mock_xqueue
(
success
=
False
):
self
.
xqueue
.
add_example_cert
(
cert
)
# Verify the error status of the certificate
self
.
assertEqual
(
cert
.
status
,
ExampleCertificate
.
STATUS_ERROR
)
self
.
assertIn
(
self
.
ERROR_MSG
,
cert
.
error_reason
)
def
_create_example_cert
(
self
):
"""Create an example certificate. """
cert_set
=
ExampleCertificateSet
.
objects
.
create
(
course_key
=
self
.
COURSE_KEY
)
return
ExampleCertificate
.
objects
.
create
(
example_cert_set
=
cert_set
,
description
=
self
.
DESCRIPTION
,
template
=
self
.
TEMPLATE
)
@contextmanager
def
_mock_xqueue
(
self
,
success
=
True
):
"""Mock the XQueue method for sending a task to the queue. """
with
patch
.
object
(
XQueueInterface
,
'send_to_queue'
)
as
mock_send
:
mock_send
.
return_value
=
(
0
,
None
)
if
success
else
(
1
,
self
.
ERROR_MSG
)
yield
mock_send
def
_assert_queue_task
(
self
,
mock_send
,
cert
):
"""Check that the task was added to the queue. """
expected_header
=
{
'lms_key'
:
cert
.
access_key
,
'lms_callback_url'
:
'https://edx.org/update_example_certificate?key={key}'
.
format
(
key
=
cert
.
uuid
),
'queue_name'
:
'certificates'
}
expected_body
=
{
'action'
:
'create'
,
'username'
:
cert
.
uuid
,
'name'
:
u'John Doë'
,
'course_id'
:
unicode
(
self
.
COURSE_KEY
),
'template_pdf'
:
'test.pdf'
,
'example_certificate'
:
True
}
self
.
assertTrue
(
mock_send
.
called
)
__
,
kwargs
=
mock_send
.
call_args_list
[
0
]
actual_header
=
json
.
loads
(
kwargs
[
'header'
])
actual_body
=
json
.
loads
(
kwargs
[
'body'
])
self
.
assertEqual
(
expected_header
,
actual_header
)
self
.
assertEqual
(
expected_body
,
actual_body
)
lms/djangoapps/certificates/tests/test_views.py
0 → 100644
View file @
984f8732
"""Tests for certificates views. """
import
json
import
ddt
from
django.test
import
TestCase
from
django.core.urlresolvers
import
reverse
from
django.core.cache
import
cache
from
opaque_keys.edx.locator
import
CourseLocator
from
certificates.models
import
ExampleCertificateSet
,
ExampleCertificate
@ddt.ddt
class
UpdateExampleCertificateViewTest
(
TestCase
):
"""Tests for the XQueue callback that updates example certificates. """
COURSE_KEY
=
CourseLocator
(
org
=
'test'
,
course
=
'test'
,
run
=
'test'
)
DESCRIPTION
=
'test'
TEMPLATE
=
'test.pdf'
DOWNLOAD_URL
=
'http://www.example.com'
ERROR_REASON
=
'Kaboom!'
def
setUp
(
self
):
super
(
UpdateExampleCertificateViewTest
,
self
)
.
setUp
()
self
.
cert_set
=
ExampleCertificateSet
.
objects
.
create
(
course_key
=
self
.
COURSE_KEY
)
self
.
cert
=
ExampleCertificate
.
objects
.
create
(
example_cert_set
=
self
.
cert_set
,
description
=
self
.
DESCRIPTION
,
template
=
self
.
TEMPLATE
,
)
self
.
url
=
reverse
(
'certificates.views.update_example_certificate'
)
# Since rate limit counts are cached, we need to clear
# this before each test.
cache
.
clear
()
def
test_update_example_certificate_success
(
self
):
response
=
self
.
_post_to_view
(
self
.
cert
,
download_url
=
self
.
DOWNLOAD_URL
)
self
.
_assert_response
(
response
)
self
.
cert
=
ExampleCertificate
.
objects
.
get
()
self
.
assertEqual
(
self
.
cert
.
status
,
ExampleCertificate
.
STATUS_SUCCESS
)
self
.
assertEqual
(
self
.
cert
.
download_url
,
self
.
DOWNLOAD_URL
)
def
test_update_example_certificate_invalid_key
(
self
):
payload
=
{
'xqueue_header'
:
json
.
dumps
({
'lms_key'
:
'invalid'
}),
'xqueue_body'
:
json
.
dumps
({
'username'
:
self
.
cert
.
uuid
,
'url'
:
self
.
DOWNLOAD_URL
})
}
response
=
self
.
client
.
post
(
self
.
url
,
data
=
payload
)
self
.
assertEqual
(
response
.
status_code
,
404
)
def
test_update_example_certificate_error
(
self
):
response
=
self
.
_post_to_view
(
self
.
cert
,
error_reason
=
self
.
ERROR_REASON
)
self
.
_assert_response
(
response
)
self
.
cert
=
ExampleCertificate
.
objects
.
get
()
self
.
assertEqual
(
self
.
cert
.
status
,
ExampleCertificate
.
STATUS_ERROR
)
self
.
assertEqual
(
self
.
cert
.
error_reason
,
self
.
ERROR_REASON
)
@ddt.data
(
'xqueue_header'
,
'xqueue_body'
)
def
test_update_example_certificate_invalid_params
(
self
,
missing_param
):
payload
=
{
'xqueue_header'
:
json
.
dumps
({
'lms_key'
:
self
.
cert
.
access_key
}),
'xqueue_body'
:
json
.
dumps
({
'username'
:
self
.
cert
.
uuid
,
'url'
:
self
.
DOWNLOAD_URL
})
}
del
payload
[
missing_param
]
response
=
self
.
client
.
post
(
self
.
url
,
data
=
payload
)
self
.
assertEqual
(
response
.
status_code
,
400
)
def
test_update_example_certificate_missing_download_url
(
self
):
payload
=
{
'xqueue_header'
:
json
.
dumps
({
'lms_key'
:
self
.
cert
.
access_key
}),
'xqueue_body'
:
json
.
dumps
({
'username'
:
self
.
cert
.
uuid
})
}
response
=
self
.
client
.
post
(
self
.
url
,
data
=
payload
)
self
.
assertEqual
(
response
.
status_code
,
400
)
def
test_update_example_cetificate_non_json_param
(
self
):
payload
=
{
'xqueue_header'
:
'{/invalid'
,
'xqueue_body'
:
'{/invalid'
}
response
=
self
.
client
.
post
(
self
.
url
,
data
=
payload
)
self
.
assertEqual
(
response
.
status_code
,
400
)
def
test_unsupported_http_method
(
self
):
response
=
self
.
client
.
get
(
self
.
url
)
self
.
assertEqual
(
response
.
status_code
,
405
)
def
test_bad_request_rate_limiting
(
self
):
payload
=
{
'xqueue_header'
:
json
.
dumps
({
'lms_key'
:
'invalid'
}),
'xqueue_body'
:
json
.
dumps
({
'username'
:
self
.
cert
.
uuid
,
'url'
:
self
.
DOWNLOAD_URL
})
}
# Exceed the rate limit for invalid requests
# (simulate a DDOS with invalid keys)
for
_
in
range
(
100
):
response
=
self
.
client
.
post
(
self
.
url
,
data
=
payload
)
if
response
.
status_code
==
403
:
break
# The final status code should indicate that the rate
# limit was exceeded.
self
.
assertEqual
(
response
.
status_code
,
403
)
def
_post_to_view
(
self
,
cert
,
download_url
=
None
,
error_reason
=
None
):
"""Simulate a callback from the XQueue to the example certificate end-point. """
header
=
{
'lms_key'
:
cert
.
access_key
}
body
=
{
'username'
:
cert
.
uuid
}
if
download_url
is
not
None
:
body
[
'url'
]
=
download_url
if
error_reason
is
not
None
:
body
[
'error'
]
=
'error'
body
[
'error_reason'
]
=
self
.
ERROR_REASON
payload
=
{
'xqueue_header'
:
json
.
dumps
(
header
),
'xqueue_body'
:
json
.
dumps
(
body
)
}
return
self
.
client
.
post
(
self
.
url
,
data
=
payload
)
def
_assert_response
(
self
,
response
):
"""Check the response from the callback end-point. """
content
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
content
[
'return_code'
],
0
)
lms/djangoapps/certificates/tests/tests_api.py
deleted
100644 → 0
View file @
d147a655
"""
Tests for the certificates api and helper function.
"""
from
django.test
import
RequestFactory
from
django.test.utils
import
override_settings
from
mock
import
patch
,
Mock
from
xmodule.modulestore.tests.factories
import
CourseFactory
from
xmodule.modulestore.tests.django_utils
import
ModuleStoreTestCase
from
certificates.api
import
certificate_downloadable_status
,
generate_user_certificates
from
student.models
import
CourseEnrollment
from
student.tests.factories
import
UserFactory
from
certificates.models
import
CertificateStatuses
from
certificates.tests.factories
import
GeneratedCertificateFactory
class
CertificateDownloadableStatusTests
(
ModuleStoreTestCase
):
"""
Tests for the certificate_downloadable_status helper function
"""
def
setUp
(
self
):
super
(
CertificateDownloadableStatusTests
,
self
)
.
setUp
()
self
.
student
=
UserFactory
()
self
.
student_no_cert
=
UserFactory
()
self
.
course
=
CourseFactory
.
create
(
org
=
'edx'
,
number
=
'verified'
,
display_name
=
'Verified Course'
)
self
.
request_factory
=
RequestFactory
()
def
test_user_cert_status_with_generating
(
self
):
"""
in case of certificate with error means means is_generating is True and is_downloadable is False
"""
GeneratedCertificateFactory
.
create
(
user
=
self
.
student
,
course_id
=
self
.
course
.
id
,
status
=
CertificateStatuses
.
generating
,
mode
=
'verified'
)
self
.
assertEqual
(
certificate_downloadable_status
(
self
.
student
,
self
.
course
.
id
),
{
'is_downloadable'
:
False
,
'is_generating'
:
True
,
'download_url'
:
None
}
)
def
test_user_cert_status_with_error
(
self
):
"""
in case of certificate with error means means is_generating is True and is_downloadable is False
"""
GeneratedCertificateFactory
.
create
(
user
=
self
.
student
,
course_id
=
self
.
course
.
id
,
status
=
CertificateStatuses
.
error
,
mode
=
'verified'
)
self
.
assertEqual
(
certificate_downloadable_status
(
self
.
student
,
self
.
course
.
id
),
{
'is_downloadable'
:
False
,
'is_generating'
:
True
,
'download_url'
:
None
}
)
def
test_user_with_out_cert
(
self
):
"""
in case of no certificate means is_generating is False and is_downloadable is False
"""
self
.
assertEqual
(
certificate_downloadable_status
(
self
.
student_no_cert
,
self
.
course
.
id
),
{
'is_downloadable'
:
False
,
'is_generating'
:
False
,
'download_url'
:
None
}
)
def
test_user_with_downloadable_cert
(
self
):
"""
in case of downloadable certificate means is_generating is False and is_downloadable is True
download_url has cert link
"""
GeneratedCertificateFactory
.
create
(
user
=
self
.
student
,
course_id
=
self
.
course
.
id
,
status
=
CertificateStatuses
.
downloadable
,
mode
=
'verified'
,
download_url
=
'www.google.com'
)
self
.
assertEqual
(
certificate_downloadable_status
(
self
.
student
,
self
.
course
.
id
),
{
'is_downloadable'
:
True
,
'is_generating'
:
False
,
'download_url'
:
'www.google.com'
}
)
class
GenerateUserCertificatesTest
(
ModuleStoreTestCase
):
"""
Tests for the generate_user_certificates helper function
"""
def
setUp
(
self
):
super
(
GenerateUserCertificatesTest
,
self
)
.
setUp
()
self
.
student
=
UserFactory
()
self
.
student_no_cert
=
UserFactory
()
self
.
course
=
CourseFactory
.
create
(
org
=
'edx'
,
number
=
'verified'
,
display_name
=
'Verified Course'
,
grade_cutoffs
=
{
'cutoff'
:
0.75
,
'Pass'
:
0.5
}
)
self
.
enrollment
=
CourseEnrollment
.
enroll
(
self
.
student
,
self
.
course
.
id
,
mode
=
'honor'
)
self
.
request_factory
=
RequestFactory
()
@override_settings
(
CERT_QUEUE
=
'certificates'
)
@patch
(
'courseware.grades.grade'
,
Mock
(
return_value
=
{
'grade'
:
'Pass'
,
'percent'
:
0.75
}))
def
test_new_cert_requests_into_xqueue_returns_generating
(
self
):
"""
mocking grade.grade and returns a summary with passing score.
new requests saves into xqueue and returns the status
"""
with
patch
(
'capa.xqueue_interface.XQueueInterface.send_to_queue'
)
as
mock_send_to_queue
:
mock_send_to_queue
.
return_value
=
(
0
,
"Successfully queued"
)
self
.
assertEqual
(
generate_user_certificates
(
self
.
student
,
self
.
course
),
'generating'
)
lms/djangoapps/certificates/views.py
View file @
984f8732
...
...
@@ -4,14 +4,21 @@ import json
import
logging
from
django.contrib.auth.models
import
User
from
django.http
import
HttpResponse
from
django.http
import
HttpResponse
,
Http404
,
HttpResponseForbidden
from
django.views.decorators.csrf
import
csrf_exempt
from
django.views.decorators.http
import
require_POST
from
capa.xqueue_interface
import
XQUEUE_METRIC_NAME
from
certificates.models
import
certificate_status_for_student
,
CertificateStatuses
,
GeneratedCertificate
from
certificates.models
import
(
certificate_status_for_student
,
CertificateStatuses
,
GeneratedCertificate
,
ExampleCertificate
)
from
certificates.queue
import
XQueueCertInterface
from
xmodule.course_module
import
CourseDescriptor
from
xmodule.modulestore.django
import
modulestore
from
util.json_request
import
JsonResponse
,
JsonResponseBadRequest
from
util.bad_request_rate_limiter
import
BadRequestRateLimiter
from
opaque_keys.edx.locations
import
SlashSeparatedCourseKey
logger
=
logging
.
getLogger
(
__name__
)
...
...
@@ -121,3 +128,99 @@ def update_certificate(request):
cert
.
save
()
return
HttpResponse
(
json
.
dumps
({
'return_code'
:
0
}),
mimetype
=
'application/json'
)
@csrf_exempt
@require_POST
def
update_example_certificate
(
request
):
"""Callback from the XQueue that updates example certificates.
Example certificates are used to verify that certificate
generation is configured correctly for a course.
Unlike other certificates, example certificates
are not associated with a particular user or displayed
to students.
For this reason, we need a different end-point to update
the status of generated example certificates.
Arguments:
request (HttpRequest)
Returns:
HttpResponse (200): Status was updated successfully.
HttpResponse (400): Invalid parameters.
HttpResponse (403): Rate limit exceeded for bad requests.
HttpResponse (404): Invalid certificate identifier or access key.
"""
logger
.
info
(
u"Received response for example certificate from XQueue."
)
rate_limiter
=
BadRequestRateLimiter
()
# Check the parameters and rate limits
# If these are invalid, return an error response.
if
rate_limiter
.
is_rate_limit_exceeded
(
request
):
logger
.
info
(
u"Bad request rate limit exceeded for update example certificate end-point."
)
return
HttpResponseForbidden
(
"Rate limit exceeded"
)
if
'xqueue_body'
not
in
request
.
POST
:
logger
.
info
(
u"Missing parameter 'xqueue_body' for update example certificate end-point"
)
rate_limiter
.
tick_bad_request_counter
(
request
)
return
JsonResponseBadRequest
(
"Parameter 'xqueue_body' is required."
)
if
'xqueue_header'
not
in
request
.
POST
:
logger
.
info
(
u"Missing parameter 'xqueue_header' for update example certificate end-point"
)
rate_limiter
.
tick_bad_request_counter
(
request
)
return
JsonResponseBadRequest
(
"Parameter 'xqueue_header' is required."
)
try
:
xqueue_body
=
json
.
loads
(
request
.
POST
[
'xqueue_body'
])
xqueue_header
=
json
.
loads
(
request
.
POST
[
'xqueue_header'
])
except
(
ValueError
,
TypeError
):
logger
.
info
(
u"Could not decode params to example certificate end-point as JSON."
)
rate_limiter
.
tick_bad_request_counter
(
request
)
return
JsonResponseBadRequest
(
"Parameters must be JSON-serialized."
)
# Attempt to retrieve the example certificate record
# so we can update the status.
try
:
uuid
=
xqueue_body
.
get
(
'username'
)
access_key
=
xqueue_header
.
get
(
'lms_key'
)
cert
=
ExampleCertificate
.
objects
.
get
(
uuid
=
uuid
,
access_key
=
access_key
)
except
ExampleCertificate
.
DoesNotExist
:
# If we are unable to retrieve the record, it means the uuid or access key
# were not valid. This most likely means that the request is NOT coming
# from the XQueue. Return a 404 and increase the bad request counter
# to protect against a DDOS attack.
logger
.
info
(
u"Could not find example certificate with uuid '
%
s' and access key '
%
s'"
,
uuid
,
access_key
)
rate_limiter
.
tick_bad_request_counter
(
request
)
raise
Http404
if
'error'
in
xqueue_body
:
# If an error occurs, save the error message so we can fix the issue.
error_reason
=
xqueue_body
.
get
(
'error_reason'
)
cert
.
update_status
(
ExampleCertificate
.
STATUS_ERROR
,
error_reason
=
error_reason
)
logger
.
warning
(
(
u"Error occurred during example certificate generation for uuid '
%
s'. "
u"The error response was '
%
s'."
),
uuid
,
error_reason
)
else
:
# If the certificate generated successfully, save the download URL
# so we can display the example certificate.
download_url
=
xqueue_body
.
get
(
'url'
)
if
download_url
is
None
:
rate_limiter
.
tick_bad_request_counter
(
request
)
logger
.
warning
(
u"No download URL provided for example certificate with uuid '
%
s'."
,
uuid
)
return
JsonResponseBadRequest
(
"Parameter 'download_url' is required for successfully generated certificates."
)
else
:
cert
.
update_status
(
ExampleCertificate
.
STATUS_SUCCESS
,
download_url
=
download_url
)
logger
.
info
(
"Successfully updated example certificate with uuid '
%
s'."
,
uuid
)
# Let the XQueue know that we handled the response
return
JsonResponse
({
'return_code'
:
0
})
lms/urls.py
View file @
984f8732
...
...
@@ -17,7 +17,9 @@ urlpatterns = (
# certificate view
url
(
r'^update_certificate$'
,
'certificates.views.update_certificate'
),
url
(
r'^update_example_certificate$'
,
'certificates.views.update_example_certificate'
),
url
(
r'^request_certificate$'
,
'certificates.views.request_certificate'
),
url
(
r'^$'
,
'branding.views.index'
,
name
=
"root"
),
# Main marketing page, or redirect to courseware
url
(
r'^dashboard$'
,
'student.views.dashboard'
,
name
=
"dashboard"
),
url
(
r'^login_ajax$'
,
'student.views.login_user'
,
name
=
"login"
),
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment