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.
|
## Course level Certificate Name overrides.
|
||||||
cert_name_short = String(
|
cert_name_short = String(
|
||||||
help=_(
|
help=_(
|
||||||
"Use this setting only when generating PDF certificates. "
|
'Use this setting only when generating PDF certificates. '
|
||||||
"Between quotation marks, enter the short name of the course to use on the certificate that "
|
'Between quotation marks, enter the short name of the type of certificate that '
|
||||||
"students receive when they complete the course."
|
'students receive when they complete the course. For instance, "Certificate".'
|
||||||
),
|
),
|
||||||
display_name=_("Certificate Name (Short)"),
|
display_name=_("Certificate Name (Short)"),
|
||||||
scope=Scope.settings,
|
scope=Scope.settings,
|
||||||
@@ -505,9 +505,9 @@ class CourseFields(object):
|
|||||||
)
|
)
|
||||||
cert_name_long = String(
|
cert_name_long = String(
|
||||||
help=_(
|
help=_(
|
||||||
"Use this setting only when generating PDF certificates. "
|
'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 "
|
'Between quotation marks, enter the long name of the type of certificate that students '
|
||||||
"receive when they complete the course."
|
'receive when they complete the course. For instance, "Certificate of Achievement".'
|
||||||
),
|
),
|
||||||
display_name=_("Certificate Name (Long)"),
|
display_name=_("Certificate Name (Long)"),
|
||||||
scope=Scope.settings,
|
scope=Scope.settings,
|
||||||
|
|||||||
Reference in New Issue
Block a user