Merge pull request #11329 from cpennington/fix-cert-type-names
Fix the help text for cert_name_short and cert_name_long
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
"""
|
||||
A single-use management command that provides an interactive way to remove
|
||||
erroneous certificate names.
|
||||
"""
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
|
||||
Result = namedtuple("Result", ["course_key", "cert_name_short", "cert_name_long", "should_clean"])
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
A management command that provides an interactive way to remove erroneous cert_name_long and
|
||||
cert_name_short course attributes across both the Split and Mongo modulestores.
|
||||
"""
|
||||
help = 'Allows manual clean-up of invalid cert_name_short and cert_name_long entries on CourseModules'
|
||||
|
||||
def _mongo_results(self):
|
||||
"""
|
||||
Return Result objects for any mongo-modulestore backend course that has
|
||||
cert_name_short or cert_name_long set.
|
||||
"""
|
||||
# N.B. This code breaks many abstraction barriers. That's ok, because
|
||||
# it's a one-time cleanup command.
|
||||
# pylint: disable=protected-access
|
||||
mongo_modulestore = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo)
|
||||
old_mongo_courses = mongo_modulestore.collection.find({
|
||||
"_id.category": "course",
|
||||
"$or": [
|
||||
{"metadata.cert_name_short": {"$exists": 1}},
|
||||
{"metadata.cert_name_long": {"$exists": 1}},
|
||||
]
|
||||
}, {
|
||||
"_id": True,
|
||||
"metadata.cert_name_short": True,
|
||||
"metadata.cert_name_long": True,
|
||||
})
|
||||
|
||||
return [
|
||||
Result(
|
||||
mongo_modulestore.make_course_key(
|
||||
course['_id']['org'],
|
||||
course['_id']['course'],
|
||||
course['_id']['name'],
|
||||
),
|
||||
course['metadata'].get('cert_name_short'),
|
||||
course['metadata'].get('cert_name_long'),
|
||||
True
|
||||
) for course in old_mongo_courses
|
||||
]
|
||||
|
||||
def _split_results(self):
|
||||
"""
|
||||
Return Result objects for any split-modulestore backend course that has
|
||||
cert_name_short or cert_name_long set.
|
||||
"""
|
||||
# N.B. This code breaks many abstraction barriers. That's ok, because
|
||||
# it's a one-time cleanup command.
|
||||
# pylint: disable=protected-access
|
||||
split_modulestore = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.split)
|
||||
active_version_collection = split_modulestore.db_connection.course_index
|
||||
structure_collection = split_modulestore.db_connection.structures
|
||||
|
||||
branches = active_version_collection.aggregate([{
|
||||
'$group': {
|
||||
'_id': 1,
|
||||
'draft': {'$push': '$versions.draft-branch'},
|
||||
'published': {'$push': '$versions.published-branch'}
|
||||
}
|
||||
}, {
|
||||
'$project': {
|
||||
'_id': 1,
|
||||
'branches': {'$setUnion': ['$draft', '$published']}
|
||||
}
|
||||
}])['result'][0]['branches']
|
||||
|
||||
structures = list(
|
||||
structure_collection.find({
|
||||
'_id': {'$in': branches},
|
||||
'blocks': {'$elemMatch': {
|
||||
'$and': [
|
||||
{"block_type": "course"},
|
||||
{'$or': [
|
||||
{'fields.cert_name_long': {'$exists': True}},
|
||||
{'fields.cert_name_short': {'$exists': True}}
|
||||
]}
|
||||
]
|
||||
}}
|
||||
}, {
|
||||
'_id': True,
|
||||
'blocks.fields.cert_name_long': True,
|
||||
'blocks.fields.cert_name_short': True,
|
||||
})
|
||||
)
|
||||
|
||||
structure_map = {struct['_id']: struct for struct in structures}
|
||||
structure_ids = [struct['_id'] for struct in structures]
|
||||
|
||||
split_mongo_courses = list(active_version_collection.find({
|
||||
'$or': [
|
||||
{"versions.draft-branch": {'$in': structure_ids}},
|
||||
{"versions.published": {'$in': structure_ids}},
|
||||
]
|
||||
}, {
|
||||
'org': True,
|
||||
'course': True,
|
||||
'run': True,
|
||||
'versions': True,
|
||||
}))
|
||||
|
||||
for course in split_mongo_courses:
|
||||
draft = course['versions'].get('draft-branch')
|
||||
if draft in structure_map:
|
||||
draft_fields = structure_map[draft]['blocks'][0].get('fields', {})
|
||||
else:
|
||||
draft_fields = {}
|
||||
|
||||
published = course['versions'].get('published')
|
||||
if published in structure_map:
|
||||
published_fields = structure_map[published]['blocks'][0].get('fields', {})
|
||||
else:
|
||||
published_fields = {}
|
||||
|
||||
for fields in (draft_fields, published_fields):
|
||||
for field in ('cert_name_short', 'cert_name_long'):
|
||||
if field in fields:
|
||||
course[field] = fields[field]
|
||||
|
||||
return [
|
||||
Result(
|
||||
split_modulestore.make_course_key(
|
||||
course['org'],
|
||||
course['course'],
|
||||
course['run'],
|
||||
),
|
||||
course.get('cert_name_short'),
|
||||
course.get('cert_name_long'),
|
||||
True
|
||||
) for course in split_mongo_courses
|
||||
]
|
||||
|
||||
def _display(self, results):
|
||||
"""
|
||||
Render a list of Result objects as a nicely formatted table.
|
||||
"""
|
||||
headers = ["Course Key", "cert_name_short", "cert_name_short", "Should clean?"]
|
||||
col_widths = [
|
||||
max(len(unicode(result[col])) for result in results + [headers])
|
||||
for col in range(len(results[0]))
|
||||
]
|
||||
id_format = "{{:>{}}} |".format(len(unicode(len(results))))
|
||||
col_format = "| {{:>{}}} |"
|
||||
|
||||
self.stdout.write(id_format.format(""), ending='')
|
||||
for header, width in zip(headers, col_widths):
|
||||
self.stdout.write(col_format.format(width).format(header), ending='')
|
||||
|
||||
self.stdout.write('')
|
||||
|
||||
for idx, result in enumerate(results):
|
||||
self.stdout.write(id_format.format(idx), ending='')
|
||||
for col, width in zip(result, col_widths):
|
||||
self.stdout.write(col_format.format(width).format(unicode(col)), ending='')
|
||||
self.stdout.write("")
|
||||
|
||||
def _commit(self, results):
|
||||
"""
|
||||
For each Result in ``results``, if ``should_clean`` is True, remove cert_name_long
|
||||
and cert_name_short from the course and save in the backing modulestore.
|
||||
"""
|
||||
for result in results:
|
||||
if not result.should_clean:
|
||||
continue
|
||||
course = modulestore().get_course(result.course_key)
|
||||
del course.cert_name_short
|
||||
del course.cert_name_long
|
||||
modulestore().update_item(course, ModuleStoreEnum.UserID.mgmt_command)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
||||
results = self._mongo_results() + self._split_results()
|
||||
|
||||
self.stdout.write("Type the index of a row to toggle whether it will be cleaned, "
|
||||
"'commit' to remove all cert_name_short and cert_name_long values "
|
||||
"from any rows marked for cleaning, or 'quit' to quit.")
|
||||
|
||||
while True:
|
||||
self._display(results)
|
||||
command = raw_input("<index>|commit|quit: ").strip()
|
||||
|
||||
if command == 'quit':
|
||||
return
|
||||
elif command == 'commit':
|
||||
self._commit(results)
|
||||
return
|
||||
elif command == '':
|
||||
continue
|
||||
else:
|
||||
index = int(command)
|
||||
results[index] = results[index]._replace(should_clean=not results[index].should_clean)
|
||||
@@ -495,9 +495,9 @@ class CourseFields(object):
|
||||
## Course level Certificate Name overrides.
|
||||
cert_name_short = String(
|
||||
help=_(
|
||||
"Use this setting only when generating PDF certificates. "
|
||||
"Between quotation marks, enter the short name of the course to use on the certificate that "
|
||||
"students receive when they complete the course."
|
||||
'Use this setting only when generating PDF certificates. '
|
||||
'Between quotation marks, enter the short name of the type of certificate that '
|
||||
'students receive when they complete the course. For instance, "Certificate".'
|
||||
),
|
||||
display_name=_("Certificate Name (Short)"),
|
||||
scope=Scope.settings,
|
||||
@@ -505,9 +505,9 @@ class CourseFields(object):
|
||||
)
|
||||
cert_name_long = String(
|
||||
help=_(
|
||||
"Use this setting only when generating PDF certificates. "
|
||||
"Between quotation marks, enter the long name of the course to use on the certificate that students "
|
||||
"receive when they complete the course."
|
||||
'Use this setting only when generating PDF certificates. '
|
||||
'Between quotation marks, enter the long name of the type of certificate that students '
|
||||
'receive when they complete the course. For instance, "Certificate of Achievement".'
|
||||
),
|
||||
display_name=_("Certificate Name (Long)"),
|
||||
scope=Scope.settings,
|
||||
|
||||
Reference in New Issue
Block a user