forked from openedx/edx-platform
-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Nborovesnkiy/edxoldmng 218/copy certificate configurations including …
…signatures to credentials on certificate update (#2464) * feat: [EDXOLDMNG-218] implements signal handlers for COURSE_CERTIFICATE_CONFIG_DELETED and COURSE_CERTIFICATE_CONFIG_CHANGED to send cert config data onto Credentials service * feat: [EDXOLDMNG-218] emits course certificate config changed signal when course import happened. * feat: [EDXOLDMNG-218] Emits emit_course_certificate_config_changed_signal, emit_course_certificate_config_deleted_signal when course config data is changing in Studio UI * test: [EDXOLDMNG-218] Tests for course certificate configuration signals. * test: [EDXOLDMNG-218] Adds tests for CertificatesListHandler and CertificatesDetailHandler to be sure that certificate config data signals are emiting * refactor: [EDXOLDMNG-218] Makes CERTIFICATE_JSON_WITH_SIGNATORIES and HelperMethods are ready to more common usage. * chore: [EDXOLDMNG-218] Code polishing. * chore: [EDXOLDMNG-218] temporarily switched to RG code repository for openedx-events package. * chore: [EDXOLDMNG-218] updates openedx-events to 3.2.0 to testing env. * style: [EDXOLDMNG-218] deletes unused import * Nborovenskiy/edxoldmng 224/create a management command to transfer certificate configs to credentials (#2465) * refactor: [EDXOLDMNG-224] Moves course certificate configuration creation and deletion to separate functions. * feat: [EDXOLDMNG-218] uses course id instead of course key for CertificateConfigData container * test: [EDXOLDMNG-224] updates SignalCourseCertificateConfigurationListenerTestCase tests in order refactoring the approach to send data to Credentials via http. * test: [EDXOLDMNG-224] creates tests for course certificate configuration credentials utils apis. * feat: [EDXOLDMNG-224] Creates manage.py command to migrate course certificate coniguration. * refactor: [EDXOLDMNG-224] code polishing for migrate cert config command. * docs: [EDXOLDMNG-224] adds doc string for migrate_cert_config command's main class
- Loading branch information
1 parent
7a0010a
commit 454c787
Showing
11 changed files
with
750 additions
and
78 deletions.
There are no files selected for viewing
255 changes: 255 additions & 0 deletions
255
cms/djangoapps/contentstore/management/commands/migrate_cert_config.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,255 @@ | ||
""" | ||
Script to transfer course certificate configuration to Credential IDA from modulestore (old mongo (draft) and slit). | ||
The script is a one-time action. | ||
The context for this decision can be read here | ||
lms/djangoapps/certificates/docs/decisions/007-transfer-certificate-signatures-from-mongo.rst | ||
""" | ||
|
||
import attr | ||
from itertools import chain | ||
from typing import Dict, Iterator, List, Union | ||
|
||
from django.core.management.base import BaseCommand, CommandError | ||
|
||
from common.djangoapps.course_modes.models import CourseMode | ||
from opaque_keys import InvalidKeyError | ||
from opaque_keys.edx.keys import CourseKey | ||
from opaque_keys.edx.locator import CourseLocator | ||
from openedx.core.djangoapps.credentials.utils import send_course_certificate_configuration | ||
from xmodule.modulestore import ModuleStoreEnum | ||
from xmodule.modulestore.django import modulestore | ||
from xmodule.course_module import CourseBlock | ||
from cms.djangoapps.contentstore.signals.handlers import ( | ||
create_course_certificate_config_data, | ||
get_certificate_signature_assets, | ||
) | ||
from cms.djangoapps.contentstore.views.certificates import CertificateManager | ||
|
||
|
||
class FakedUser: | ||
def __init__(self, id_: int): | ||
self.id = id_ | ||
|
||
|
||
class FakedRequest: | ||
def __init__(self, user_id: int): | ||
self.user = FakedUser(user_id) | ||
|
||
|
||
class Command(BaseCommand): | ||
""" | ||
Management command to transfer course certificate configuration from modulestore to Credentials IDA. | ||
Examples: | ||
./manage.py migrate_cert_config <course_id_1> <course_id_2> - transfer courses with provided keys | ||
./manage.py migrate_cert_config --all - transfer all available courses | ||
./manage.py migrate_cert_config --draft - transfer all mongo(old approach) modulestore available courses | ||
./manage.py migrate_cert_config --draft - transfer all split(new approach) modulestore available courses | ||
./manage.py migrate_cert_config --all --delete-after - transfer all available courses | ||
and delete course certificate configuration, signature assets from modulestore after successfull transfer. | ||
""" | ||
|
||
help = 'Allows manual transfer course certificate configuration from modulestore to Credentials IDA.' | ||
|
||
def add_arguments(self, parser): | ||
parser.add_argument('course_ids', nargs='*', metavar='course_id') | ||
parser.add_argument( | ||
'--course_storage_type', | ||
type=str.lower, | ||
default=None, | ||
choices=['all', 'draft', 'split'], | ||
help='Course storage types whose certificate configurations are to be migrated.', | ||
) | ||
parser.add_argument( | ||
'--delete-after', | ||
help='Boolean value to delete course certificate configuration, signature assets from modulestore.', | ||
action='store_true', | ||
) | ||
|
||
def _parse_course_key(self, raw_value: str) -> CourseKey: | ||
""" | ||
Parses course key from string | ||
""" | ||
try: | ||
result = CourseKey.from_string(raw_value) | ||
except InvalidKeyError: | ||
raise CommandError(f'Invalid course_key: {raw_value}.') | ||
if not isinstance(result, CourseLocator): | ||
raise CommandError(f'Argument {raw_value} is not a course key') | ||
|
||
return result | ||
|
||
def get_mongo_courses(self) -> Iterator[CourseKey]: | ||
""" | ||
Return objects for any mongo(old approach) modulestore backend course that has a certificate configuration. | ||
""" | ||
# N.B. This code breaks many abstraction barriers. That's ok, because | ||
# it's a one-time cleanup command. | ||
mongo_modulestore = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo) | ||
old_mongo_courses = mongo_modulestore.collection.find( | ||
{'_id.category': 'course', 'metadata.certificates': {'$exists': 1}}, | ||
{ | ||
'_id': True, | ||
}, | ||
) | ||
for course in old_mongo_courses: | ||
yield mongo_modulestore.make_course_key( | ||
course['_id']['org'], | ||
course['_id']['course'], | ||
course['_id']['name'], | ||
) | ||
|
||
def get_split_courses(self) -> Iterator[CourseKey]: | ||
""" | ||
Return objects for any split modulestore backend course that has a certificate configuration. | ||
""" | ||
# N.B. This code breaks many abstraction barriers. That's ok, because | ||
# it's a one-time cleanup command. | ||
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 = list( | ||
active_version_collection.aggregate( | ||
[ | ||
{ | ||
'$group': { | ||
'_id': 1, | ||
'draft': {'$push': '$versions.draft-branch'}, | ||
'published': {'$push': '$versions.published-branch'}, | ||
} | ||
}, | ||
{'$project': {'_id': 1, 'branches': {'$setUnion': ['$draft', '$published']}}}, | ||
] | ||
) | ||
)[0]['branches'] | ||
|
||
structures = structure_collection.find( | ||
{ | ||
'_id': {'$in': branches}, | ||
'blocks': { | ||
'$elemMatch': { | ||
'$and': [ | ||
{'block_type': 'course'}, | ||
{'fields.certificates': {'$exists': True}}, | ||
] | ||
} | ||
}, | ||
}, | ||
{ | ||
'_id': True, | ||
}, | ||
) | ||
|
||
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: | ||
yield split_modulestore.make_course_key( | ||
course['org'], | ||
course['course'], | ||
course['run'], | ||
) | ||
|
||
def send_to_credentials( | ||
self, | ||
course_key: CourseKey, | ||
mode: CourseMode, | ||
certificate_data: Dict[str, Union[str, List[str]]] | ||
): | ||
""" | ||
Sends certificate configuration data to Credential IDA via http request. | ||
""" | ||
certificate_config = create_course_certificate_config_data(str(course_key), mode.slug, certificate_data) | ||
files_to_upload = dict(get_certificate_signature_assets(certificate_config)) | ||
certificate_config_data = attr.asdict(certificate_config) | ||
send_course_certificate_configuration(str(course_key), certificate_config_data, files_to_upload) | ||
|
||
def delete_from_store(self, course: CourseBlock, certificates: List[Dict[str, str]]): | ||
""" | ||
Deletes certificate configuration from modulestore storage. | ||
""" | ||
store = modulestore() | ||
request = FakedRequest(ModuleStoreEnum.UserID.mgmt_command) | ||
for cert in certificates: | ||
CertificateManager.remove_certificate( | ||
request=request, store=store, course=course, certificate_id=cert['id'] | ||
) | ||
|
||
def validate_input(self, options: Dict[str, str]): | ||
""" | ||
Makes manage-command input validation. Raises CommandError if has conflicts. | ||
""" | ||
if (not len(options['course_ids']) and not options.get('course_storage_type')) or ( | ||
len(options['course_ids']) and options.get('course_storage_type') | ||
): | ||
raise CommandError( | ||
'Certificate configurations migration requires one or more <course_id>s ' | ||
'OR the --course_storage_type choice.' | ||
) | ||
|
||
def get_course_keys_by_option(self, options: Dict[str, str]) -> Iterator[CourseKey]: | ||
storage_type = options['course_storage_type'] | ||
course_ids = options['course_ids'] | ||
if storage_type: | ||
if storage_type == 'all': | ||
return chain(self.get_mongo_courses(), self.get_split_courses()) | ||
elif storage_type == 'draft': | ||
return self.get_mongo_courses() | ||
elif storage_type == 'split': | ||
return self.get_split_courses() | ||
if course_ids: | ||
return map(self._parse_course_key, course_ids) | ||
|
||
def migrate(self, course_keys: List[CourseKey], options: Dict[str, str]): | ||
""" | ||
Main entry point for executiong all migration-related actions. | ||
Sending to Credential and/or removal from storage. | ||
If there are problems with some course, i.e. or it does not exist, or the course is not set to mode, | ||
which allows to have a certificate, or no certificate configuration, | ||
then an user of this command will be notified by a message. | ||
""" | ||
for course_key in course_keys: | ||
if course := modulestore().get_course(course_key): | ||
if course_modes := CourseMode.objects.filter( | ||
course_id=course_key, mode_slug__in=CourseMode.CERTIFICATE_RELEVANT_MODES | ||
): | ||
if certificates := CertificateManager.get_certificates(course): | ||
for certificate_data in certificates: | ||
for mode in course_modes: | ||
try: | ||
self.send_to_credentials(course_key, mode, certificate_data) | ||
except Exception as exc: | ||
self.stderr.write(str(exc)) | ||
else: | ||
if options.get('delete_after'): | ||
self.delete_from_store(course, certificates) | ||
else: | ||
self.stderr.write(f'The course {course_key} does not have any configured certificates.') | ||
else: | ||
self.stderr.write(f'The course {course_key} does not have certificate relevant modes.') | ||
else: | ||
self.stderr.write(f'The course {course_key} does not exist.') | ||
|
||
def handle(self, *args, **options): | ||
""" | ||
Executes the command. | ||
""" | ||
self.validate_input(options) | ||
course_keys_to_migrate = self.get_course_keys_by_option(options) | ||
self.migrate(course_keys_to_migrate, options) |
Oops, something went wrong.