Beispiel #1
0
 def __init__(self, user, enterprise_configuration):
     """
     Initialize the exporter.
     """
     super().__init__(user, enterprise_configuration)
     self.enterprise_api = EnterpriseApiClient(self.user)
     self.enterprise_catalog_api = EnterpriseCatalogApiClient(self.user)
Beispiel #2
0
def update_enterprise_catalog_data(sender, instance, **kwargs):  # pylint: disable=unused-argument
    """
    Send data changes to Enterprise Catalogs to the Enterprise Catalog Service.

    Additionally sends a request to update the catalog's metadata from discovery, and index any relevant content for
    Algolia.
    """
    catalog_uuid = instance.uuid
    try:
        catalog_client = EnterpriseCatalogApiClient()
        if kwargs['created']:
            response = catalog_client.get_enterprise_catalog(
                catalog_uuid=catalog_uuid,
                # Suppress 404 exception on create since we do not expect the catalog
                # to exist yet in enterprise-catalog
                should_raise_exception=False,
            )
        else:
            response = catalog_client.get_enterprise_catalog(
                catalog_uuid=catalog_uuid)
    except NotConnectedToOpenEdX as exc:
        logger.exception('Unable to update Enterprise Catalog {}'.format(
            str(catalog_uuid)),
                         exc_info=exc)
    else:
        if not response:
            # catalog with matching uuid does NOT exist in enterprise-catalog
            # service, so we should create a new catalog
            catalog_client.create_enterprise_catalog(
                str(catalog_uuid),
                str(instance.enterprise_customer.uuid),
                instance.enterprise_customer.name,
                instance.title,
                instance.content_filter,
                instance.enabled_course_modes,
                instance.publish_audit_enrollment_urls,
            )
        else:
            # catalog with matching uuid does exist in enterprise-catalog
            # service, so we should update the existing catalog
            update_fields = {
                'enterprise_customer':
                str(instance.enterprise_customer.uuid),
                'enterprise_customer_name':
                instance.enterprise_customer.name,
                'title':
                instance.title,
                'content_filter':
                instance.content_filter,
                'enabled_course_modes':
                instance.enabled_course_modes,
                'publish_audit_enrollment_urls':
                instance.publish_audit_enrollment_urls,
            }
            catalog_client.update_enterprise_catalog(catalog_uuid,
                                                     **update_fields)
        # Refresh catalog on all creates and updates
        catalog_client.refresh_catalogs([instance])
Beispiel #3
0
def delete_enterprise_catalog_data(sender, instance, **kwargs):  # pylint: disable=unused-argument
    """
    Send deletions of Enterprise Catalogs to the Enterprise Catalog Service.
    """
    catalog_uuid = instance.uuid
    try:
        catalog_client = EnterpriseCatalogApiClient()
        catalog_client.delete_enterprise_catalog(catalog_uuid)
    except NotConnectedToOpenEdX as exc:
        logger.exception(
            'Unable to delete Enterprise Catalog {}'.format(str(catalog_uuid)),
            exc)
Beispiel #4
0
    def delete_model(self, request, obj):
        """
        Deletes the corresponding catalog in the enterprise-catalog IDA

        From the warning in https://docs.djangoproject.com/en/1.11/ref/contrib/admin/#modeladmin-methods, we don't
        prevent the deletion of the catalog if the response fails, but periodically check the logs to clean up any
        catalog that did not get deleted from the new service while we transition.
        """
        catalog_uuid = obj.uuid
        catalog_client = EnterpriseCatalogApiClient(user=request.user)
        catalog_client.delete_enterprise_catalog(catalog_uuid)

        super(EnterpriseCustomerCatalogAdmin, self).delete_model(request, obj)
Beispiel #5
0
 def preview_catalog_url(self, obj):
     """
     Return enterprise catalog url for preview.
     """
     catalog_content_metadata_url = \
         EnterpriseCatalogApiClient.get_content_metadata_url(obj.uuid)
     return format_html('<a href="{url}" target="_blank">Preview</a>',
                        url=catalog_content_metadata_url)
Beispiel #6
0
 def change_view(self, request, object_id, form_url='', extra_context=None):
     catalog_uuid = EnterpriseCustomerCatalogAdminForm.get_catalog_preview_uuid(
         request.POST)
     if catalog_uuid:
         catalog_content_metadata_url = \
             EnterpriseCatalogApiClient.get_content_metadata_url(catalog_uuid)
         return HttpResponseRedirect(catalog_content_metadata_url)
     return super(EnterpriseCustomerAdmin,
                  self).change_view(request,
                                    object_id,
                                    form_url,
                                    extra_context=extra_context)
Beispiel #7
0
    def save_model(self, request, obj, form, change):
        """
        On save, creates or updates the corresponding catalog in the enterprise-catalog IDA.

        `change` indicates whether or not the object is being updated (as opposed to created).
        """
        catalog_uuid = obj.uuid
        catalog_client = EnterpriseCatalogApiClient(user=request.user)

        if change:
            update_fields = {
                'enterprise_customer': str(obj.enterprise_customer.uuid),
                'title': obj.title,
                'content_filter': obj.content_filter,
                'enabled_course_modes': obj.enabled_course_modes,
                'publish_audit_enrollment_urls':
                obj.publish_audit_enrollment_urls,
            }
            catalog_client.update_enterprise_catalog(catalog_uuid,
                                                     **update_fields)
        else:
            catalog_client.create_enterprise_catalog(
                str(catalog_uuid),
                str(obj.enterprise_customer.uuid),
                obj.title,
                obj.content_filter,
                obj.enabled_course_modes,
                obj.publish_audit_enrollment_urls,
            )

        super(EnterpriseCustomerCatalogAdmin,
              self).save_model(request, obj, form, change)
Beispiel #8
0
def update_enterprise_catalog_data(sender, instance, **kwargs):  # pylint: disable=unused-argument
    """
    Send data changes to Enterprise Catalogs to the Enterprise Catalog Service.
    """
    catalog_uuid = instance.uuid
    try:
        catalog_client = EnterpriseCatalogApiClient()
        response = catalog_client.get_enterprise_catalog(catalog_uuid)
    except NotConnectedToOpenEdX as exc:
        logger.exception(
            'Unable to update Enterprise Catalog {}'.format(str(catalog_uuid)),
            exc)
    else:
        if not response:
            # catalog with matching uuid does NOT exist in enterprise-catalog
            # service, so we should create a new catalog
            catalog_client.create_enterprise_catalog(
                str(catalog_uuid),
                str(instance.enterprise_customer.uuid),
                instance.enterprise_customer.name,
                instance.title,
                instance.content_filter,
                instance.enabled_course_modes,
                instance.publish_audit_enrollment_urls,
            )
        else:
            # catalog with matching uuid does exist in enterprise-catalog
            # service, so we should update the existing catalog
            update_fields = {
                'enterprise_customer':
                str(instance.enterprise_customer.uuid),
                'enterprise_customer_name':
                instance.enterprise_customer.name,
                'title':
                instance.title,
                'content_filter':
                instance.content_filter,
                'enabled_course_modes':
                instance.enabled_course_modes,
                'publish_audit_enrollment_urls':
                instance.publish_audit_enrollment_urls,
            }
            catalog_client.update_enterprise_catalog(catalog_uuid,
                                                     **update_fields)
Beispiel #9
0
    def handle(self, *args, **options):
        api_username = options['api_user']
        try:
            user = User.objects.get(username=api_username)
        except User.DoesNotExist:
            raise CommandError(
                _('A user with the username {username} was not found.').format(
                    username=api_username))

        client = EnterpriseCatalogApiClient(user=user)

        catalog_uuids_string = options.get('catalog_uuids')
        if catalog_uuids_string:
            catalog_uuids_list = catalog_uuids_string.split(',')
            queryset = EnterpriseCustomerCatalog.objects.filter(
                uuid__in=catalog_uuids_list)
        else:
            queryset = EnterpriseCustomerCatalog.objects.all()

        for enterprise_catalog in queryset:
            LOGGER.info('Migrating Enterprise Catalog {}'.format(
                enterprise_catalog.uuid))
            try:
                response = client.get_enterprise_catalog(
                    enterprise_catalog.uuid)
                if not response:
                    # catalog with matching uuid does NOT exist in enterprise-catalog
                    # service, so we should create a new catalog
                    client.create_enterprise_catalog(
                        str(enterprise_catalog.uuid),
                        str(enterprise_catalog.enterprise_customer.uuid),
                        enterprise_catalog.enterprise_customer.name,
                        enterprise_catalog.title,
                        enterprise_catalog.content_filter,
                        enterprise_catalog.enabled_course_modes,
                        enterprise_catalog.publish_audit_enrollment_urls,
                    )
                else:
                    # catalog with matching uuid does exist in enterprise-catalog
                    # service, so we should update the existing catalog
                    update_fields = {
                        'enterprise_customer':
                        str(enterprise_catalog.enterprise_customer.uuid),
                        'enterprise_customer_name':
                        enterprise_catalog.enterprise_customer.name,
                        'title':
                        enterprise_catalog.title,
                        'content_filter':
                        enterprise_catalog.content_filter,
                        'enabled_course_modes':
                        enterprise_catalog.enabled_course_modes,
                        'publish_audit_enrollment_urls':
                        enterprise_catalog.publish_audit_enrollment_urls,
                    }
                    client.update_enterprise_catalog(
                        str(enterprise_catalog.uuid),
                        **update_fields,
                    )
                LOGGER.info(
                    'Successfully migrated Enterprise Catalog {}'.format(
                        enterprise_catalog.uuid))
            except Exception:  # pylint: disable=broad-except
                LOGGER.exception(
                    'Failed to migrate enterprise catalog {}'.format(
                        enterprise_catalog.uuid))
Beispiel #10
0
class ContentMetadataExporter(Exporter):
    """
    Base class for content metadata exporters.
    """

    # DATA_TRANSFORM_MAPPING is used to map the content metadata field names expected by the integrated channel
    # to the edX content metadata schema. The values contained in the dict will be used as keys to access values
    # in each content metadata item dict which is being exported.
    #
    # Example:
    #     {
    #         'contentID': 'key',
    #         'courseTitle': 'title'
    #     }
    #
    #     Defines a transformation of the content metadata item to:
    #
    #     {
    #         'contentID': content_metadata_item['key'],
    #         'courseTitle': content_metadata_item['title']
    #     }
    #
    # Subclasses should override this class variable. By default, the edX content metadata schema is returned in
    # its entirety.
    #
    # In addition, subclasses can implement transform functions which receive a content metadata item for more
    # complicated field transformations. These functions can be content type-specific or generic for all content
    # types.
    #
    # Example:
    #     DATA_TRANSFORM_MAPPING = {
    #         'coursePrice': 'price'
    #     }
    #     # Content type-specific transformer
    #     def transform_course_price(self, course):
    #         return course['course_runs'][0]['seats']['verified]['price']
    #     # Generic transformer
    #     def transform_provider_id(self, course):
    #         return self.enterprise_configuration.provider_id
    #
    # TODO: Move this to the EnterpriseCustomerPluginConfiguration model as a JSONField.
    DATA_TRANSFORM_MAPPING = {}
    SKIP_KEY_IF_NONE = False

    def __init__(self, user, enterprise_configuration):
        """
        Initialize the exporter.
        """
        super().__init__(user, enterprise_configuration)
        self.enterprise_api = EnterpriseApiClient(self.user)
        self.enterprise_catalog_api = EnterpriseCatalogApiClient(self.user)

    def export(self, **kwargs):
        """
        Return the exported and transformed content metadata as a dictionary.
        """
        content_metadata_export = {}
        content_metadata_items = self.enterprise_catalog_api.get_content_metadata(
            self.enterprise_customer,
            enterprise_catalogs=self.enterprise_configuration.
            customer_catalogs_to_transmit)
        LOGGER.info(
            'Getting metadata for Enterprise [%s], Catalogs [%s] from Enterprise Catalog Service. Results: [%s]',
            self.enterprise_customer.name,
            self.enterprise_configuration.customer_catalogs_to_transmit,
            json.dumps(content_metadata_items))
        for item in content_metadata_items:
            transformed = self._transform_item(item)
            LOGGER.debug(
                'Exporting content metadata item with plugin configuration [%s]: [%s]',
                self.enterprise_configuration,
                json.dumps(transformed, indent=4),
            )
            content_metadata_item_export = ContentMetadataItemExport(
                item, transformed)
            content_metadata_export[content_metadata_item_export.
                                    content_id] = content_metadata_item_export
        return OrderedDict(sorted(content_metadata_export.items()))

    def _transform_item(self, content_metadata_item):
        """
        Transform the provided content metadata item to the schema expected by the integrated channel.
        """
        content_metadata_type = content_metadata_item['content_type']
        transformed_item = {}
        for integrated_channel_schema_key, edx_data_schema_key in self.DATA_TRANSFORM_MAPPING.items(
        ):
            # Look for transformer functions defined on subclasses.
            # Favor content type-specific functions.
            transformer = (getattr(
                self, 'transform_{content_type}_{edx_data_schema_key}'.format(
                    content_type=content_metadata_type,
                    edx_data_schema_key=edx_data_schema_key), None) or getattr(
                        self, 'transform_{edx_data_schema_key}'.format(
                            edx_data_schema_key=edx_data_schema_key), None))
            if transformer:
                transformed_value = transformer(content_metadata_item)  # pylint: disable=not-callable
            else:
                # The concrete subclass does not define an override for the given field,
                # so just use the data key to index the content metadata item dictionary.
                try:
                    transformed_value = content_metadata_item[
                        edx_data_schema_key]
                except KeyError:
                    # There may be a problem with the DATA_TRANSFORM_MAPPING on
                    # the concrete subclass or the concrete subclass does not implement
                    # the appropriate field transformer function.
                    LOGGER.exception(
                        'Failed to transform content metadata item field [%s] for [%s]: [%s]',
                        edx_data_schema_key,
                        self.enterprise_customer.name,
                        content_metadata_item,
                    )
                    continue

            if transformed_value is None and self.SKIP_KEY_IF_NONE:
                continue
            transformed_item[integrated_channel_schema_key] = transformed_value

        return transformed_item