-
Notifications
You must be signed in to change notification settings - Fork 4.3k
refactor: convert function based view to class based #37047
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
309 changes: 309 additions & 0 deletions
309
cms/djangoapps/contentstore/views/certificate_manager.py
This file contains hidden or 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,309 @@ | ||
| """ | ||
| Certificate Manager. | ||
| """ | ||
| import json | ||
| import logging | ||
|
|
||
| from django.conf import settings | ||
| from django.utils.translation import gettext as _ | ||
|
|
||
| from common.djangoapps.course_modes.models import CourseMode | ||
| from eventtracking import tracker | ||
| from opaque_keys import InvalidKeyError | ||
| from opaque_keys.edx.keys import AssetKey | ||
| from .assets import delete_asset | ||
|
|
||
| from common.djangoapps.util.db import MYSQL_MAX_INT, generate_int_id | ||
|
|
||
| from ..exceptions import AssetNotFoundException | ||
|
|
||
| CERTIFICATE_SCHEMA_VERSION = 1 | ||
| CERTIFICATE_MINIMUM_ID = 100 | ||
|
|
||
| LOGGER = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class CertificateException(Exception): | ||
| """ | ||
| Base exception for Certificates workflows | ||
| """ | ||
| pass # lint-amnesty, pylint: disable=unnecessary-pass | ||
|
|
||
|
|
||
| class CertificateValidationError(CertificateException): | ||
| """ | ||
| An exception raised when certificate information is invalid. | ||
| """ | ||
| pass # lint-amnesty, pylint: disable=unnecessary-pass | ||
|
|
||
|
|
||
| def _delete_asset(course_key, asset_key_string): | ||
| """ | ||
| Internal method used to create asset key from string and | ||
| remove asset by calling delete_asset method of assets module. | ||
| """ | ||
| if asset_key_string: | ||
| try: | ||
| asset_key = AssetKey.from_string(asset_key_string) | ||
| except InvalidKeyError: | ||
| # remove first slash in asset path | ||
| # otherwise it generates InvalidKeyError in case of split modulestore | ||
| if '/' == asset_key_string[0]: | ||
| asset_key_string = asset_key_string[1:] | ||
| try: | ||
| asset_key = AssetKey.from_string(asset_key_string) | ||
| except InvalidKeyError: | ||
| # Unable to parse the asset key, log and return | ||
| LOGGER.info( | ||
| "In course %r, unable to parse asset key %r, not attempting to delete signatory.", | ||
| course_key, | ||
| asset_key_string, | ||
| ) | ||
| return | ||
| else: | ||
| # Unable to parse the asset key, log and return | ||
| LOGGER.info( | ||
| "In course %r, unable to parse asset key %r, not attempting to delete signatory.", | ||
| course_key, | ||
| asset_key_string, | ||
| ) | ||
| return | ||
|
|
||
| try: | ||
| delete_asset(course_key, asset_key) | ||
| # If the asset was not found, it doesn't have to be deleted... | ||
| except AssetNotFoundException: | ||
| pass | ||
|
|
||
|
|
||
| class CertificateManager: | ||
| """ | ||
| The CertificateManager is responsible for storage, retrieval, and manipulation of Certificates | ||
| Certificates are not stored in the Django ORM, they are a field/setting on the course block | ||
| """ | ||
| @staticmethod | ||
| def parse(json_string): | ||
| """ | ||
| Deserialize the provided JSON data into a standard Python object | ||
| """ | ||
| try: | ||
| certificate = json.loads(json_string) | ||
| except ValueError: | ||
| raise CertificateValidationError(_("invalid JSON")) # lint-amnesty, pylint: disable=raise-missing-from | ||
| # Include the data contract version | ||
| certificate["version"] = CERTIFICATE_SCHEMA_VERSION | ||
| # Ensure a signatories list is always returned | ||
| if certificate.get("signatories") is None: | ||
| certificate["signatories"] = [] | ||
| certificate["editing"] = False | ||
| return certificate | ||
|
|
||
| @staticmethod | ||
| def validate(certificate_data): | ||
| """ | ||
| Ensure the certificate data contains all of the necessary fields and the values match our rules | ||
| """ | ||
| # Ensure the schema version meets our expectations | ||
| if certificate_data.get("version") != CERTIFICATE_SCHEMA_VERSION: | ||
| raise TypeError( | ||
| "Unsupported certificate schema version: {}. Expected version: {}.".format( | ||
| certificate_data.get("version"), | ||
| CERTIFICATE_SCHEMA_VERSION | ||
| ) | ||
| ) | ||
| if not certificate_data.get("name"): | ||
| raise CertificateValidationError(_("must have name of the certificate")) | ||
|
|
||
| @staticmethod | ||
| def is_activated(course): | ||
| """ | ||
| Returns whether certificates are activated for the given course, | ||
| along with the certificates. | ||
| """ | ||
| is_active = False | ||
| certificates = None | ||
| if settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False): | ||
| certificates = CertificateManager.get_certificates(course) | ||
| # we are assuming only one certificate in certificates collection. | ||
| for certificate in certificates: | ||
| is_active = certificate.get('is_active', False) | ||
| break | ||
| return is_active, certificates | ||
|
|
||
| @staticmethod | ||
| def get_used_ids(course): | ||
| """ | ||
| Return a list of certificate identifiers that are already in use for this course | ||
| """ | ||
| if not course.certificates or not course.certificates.get('certificates'): | ||
| return [] | ||
| return [cert['id'] for cert in course.certificates['certificates']] | ||
|
|
||
| @staticmethod | ||
| def assign_id(course, certificate_data, certificate_id=None): | ||
| """ | ||
| Assign an identifier to the provided certificate data. | ||
| If the caller did not provide an identifier, we autogenerate a unique one for them | ||
| In addition, we check the certificate's signatories and ensure they also have unique ids | ||
| """ | ||
| used_ids = CertificateManager.get_used_ids(course) | ||
| if certificate_id: | ||
| certificate_data['id'] = int(certificate_id) | ||
| else: | ||
| certificate_data['id'] = generate_int_id( | ||
| CERTIFICATE_MINIMUM_ID, | ||
| MYSQL_MAX_INT, | ||
| used_ids | ||
| ) | ||
|
|
||
| for index, signatory in enumerate(certificate_data['signatories']): # pylint: disable=unused-variable | ||
| if signatory and not signatory.get('id', False): | ||
| signatory['id'] = generate_int_id(used_ids=used_ids) | ||
| used_ids.append(signatory['id']) | ||
|
|
||
| return certificate_data | ||
|
|
||
| @staticmethod | ||
| def serialize_certificate(certificate): | ||
| """ | ||
| Serialize the Certificate object's locally-stored certificate data to a JSON representation | ||
| We use direct access here for specific keys in order to enforce their presence | ||
| """ | ||
| certificate_data = certificate.certificate_data | ||
| certificate_response = { | ||
| "id": certificate_data['id'], | ||
| "name": certificate_data['name'], | ||
| "description": certificate_data['description'], | ||
| "is_active": certificate_data['is_active'], | ||
| "version": CERTIFICATE_SCHEMA_VERSION, | ||
| "signatories": certificate_data['signatories'] | ||
| } | ||
|
|
||
| # Some keys are not required, such as the title override... | ||
| if certificate_data.get('course_title'): | ||
| certificate_response["course_title"] = certificate_data['course_title'] | ||
|
|
||
| return certificate_response | ||
|
|
||
| @staticmethod | ||
| def deserialize_certificate(course, value): | ||
| """ | ||
| Deserialize from a JSON representation into a Certificate object. | ||
| 'value' should be either a Certificate instance, or a valid JSON string | ||
| """ | ||
| if isinstance(value, bytes): | ||
| value = value.decode('utf-8') | ||
|
|
||
| # Ensure the schema fieldset meets our expectations | ||
| for key in ("name", "description", "version"): | ||
| if key not in value: | ||
| raise CertificateValidationError(_("Certificate dict {0} missing value key '{1}'").format(value, key)) | ||
|
|
||
| # Load up the Certificate data | ||
| certificate_data = CertificateManager.parse(value) | ||
| CertificateManager.validate(certificate_data) | ||
| certificate_data = CertificateManager.assign_id(course, certificate_data, certificate_data.get('id', None)) | ||
| certificate = Certificate(course, certificate_data) | ||
|
|
||
| # Return a new Certificate object instance | ||
| return certificate | ||
|
|
||
| @staticmethod | ||
| def get_certificates(course, only_active=False): | ||
| """ | ||
| Retrieve the certificates list from the provided course, | ||
| if `only_active` is True it would skip inactive certificates. | ||
| """ | ||
| # The top-level course field is 'certificates', which contains various properties, | ||
| # including the actual 'certificates' list that we're working with in this context | ||
| certificates = course.certificates.get('certificates', []) | ||
| if only_active: | ||
| certificates = [certificate for certificate in certificates if certificate.get('is_active', False)] | ||
| return certificates | ||
|
|
||
| @staticmethod | ||
| def get_course_modes(course): | ||
| """ | ||
| Retrieve certificate modes for the given course, | ||
| including expired modes but excluding audit mode. | ||
| """ | ||
| course_modes = [ | ||
| mode.slug for mode in CourseMode.modes_for_course( | ||
| course=course, include_expired=True | ||
| ) if mode.slug != CourseMode.AUDIT | ||
| ] | ||
| return course_modes | ||
|
|
||
| @staticmethod | ||
| def is_enabled(course): | ||
| """ | ||
| Is enabled when there is at least one course mode for the given course, | ||
| including expired modes but excluding audit mode | ||
| """ | ||
| course_modes = CertificateManager.get_course_modes(course) | ||
| return len(course_modes) > 0 | ||
|
|
||
| @staticmethod | ||
| def remove_certificate(request, store, course, certificate_id): | ||
| """ | ||
| Remove certificate from the course | ||
| """ | ||
| for index, cert in enumerate(course.certificates['certificates']): | ||
| if int(cert['id']) == int(certificate_id): | ||
| certificate = course.certificates['certificates'][index] | ||
| # Remove any signatory assets prior to dropping the entire cert record from the course | ||
| for sig_index, signatory in enumerate(certificate.get('signatories')): # pylint: disable=unused-variable | ||
| _delete_asset(course.id, signatory['signature_image_path']) | ||
| # Now drop the certificate record | ||
| course.certificates['certificates'].pop(index) | ||
| store.update_item(course, request.user.id) | ||
| break | ||
|
|
||
| # pylint-disable: unused-variable | ||
| @staticmethod | ||
| def remove_signatory(request, store, course, certificate_id, signatory_id): | ||
| """ | ||
| Remove the specified signatory from the provided course certificate | ||
| """ | ||
| for cert_index, cert in enumerate(course.certificates['certificates']): # pylint: disable=unused-variable | ||
| if int(cert['id']) == int(certificate_id): | ||
| for sig_index, signatory in enumerate(cert.get('signatories')): | ||
| if int(signatory_id) == int(signatory['id']): | ||
| _delete_asset(course.id, signatory['signature_image_path']) | ||
| del cert['signatories'][sig_index] | ||
| store.update_item(course, request.user.id) | ||
| break | ||
|
|
||
| @staticmethod | ||
| def track_event(event_name, event_data): | ||
| """Track certificate configuration event. | ||
|
|
||
| Arguments: | ||
| event_name (str): Name of the event to be logged. | ||
| event_data (dict): A Dictionary containing event data | ||
| Returns: | ||
| None | ||
|
|
||
| """ | ||
| event_name = '.'.join(['edx', 'certificate', 'configuration', event_name]) | ||
| tracker.emit(event_name, event_data) | ||
|
|
||
|
|
||
| class Certificate: | ||
| """ | ||
| The logical representation of an individual course certificate | ||
| """ | ||
| def __init__(self, course, certificate_data): | ||
| """ | ||
| Instantiate a Certificate object instance using the provided information. | ||
| """ | ||
| self.course = course | ||
| self._certificate_data = certificate_data | ||
| self.id = certificate_data['id'] # pylint: disable=invalid-name | ||
|
|
||
| @property | ||
| def certificate_data(self): | ||
| """ | ||
| Retrieve the locally-stored certificate data from the Certificate object via a helper method | ||
| """ | ||
| return self._certificate_data | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.