Badgr - Badging Support

About the path error, I cannot really comment, I cannot debug it now myself. It also seems to be caused by a missing certificate?

Can you check if your learner has a generated certificate object in /admin/certificates/generatedcertificate/?

If yes, you can try to delete it and go through the testing again. You might have to also reset the learner state in staff debug in the LMS if you are re-testing with the same account. Additionally, try with a fresh user account.

If not, then there is probably still something to solve in regards of generating certificates.

@mrtmm So we have certificates generated in the dashboard, and have tried with multiple accounts:

Or any method of getting more detailed logging without changing the source code would help us know what file path it is looking for.

Hello all :sun_with_face:
i’m collaborating with @MrJohnson-SixNinesIT on this topic. For the benefit of anyone reading this thread, we were able to confirm that the certificates service as well as badging are correctly configured. It turns out that the problem, first surfaced by @MrJohnson-SixNinesIT nov-22, is that their installation uses Django Storages Amazon S3 backend, which does not implement the image.path property referenced in the default badges backend. See def path(self, name) for more clarity on why this would have needed to have been implemented by Amazon S3 in order for the default Badges backend to work.

THE SOLUTION:
We added a custom Amazon S3 compatible badges backend to the Cookiecutter Open edX Plugin by subclassing the original badgr.py for the sole purpose of replacing the existing file access methodology with a http request to an S3 bucket or Cloudfront distribution as the case may be.

Here’s the complete source for the backend:

# python stuff
import logging
import requests

# django stuff
from django.conf import settings
from django.core.validators import URLValidator
from django.core.exceptions import ValidationError

# openedx stuff
from lms.djangoapps.badges.backends.badgr import BadgrBackend

log = logging.getLogger(__name__)


class BadgrBoto3Backend(BadgrBackend):
    def __init__(self):
        super().__init__()
        log.info("cookiecutter_plugin.badges.backends.badgr_boto3.BadgrBoto3Backend - ready.")

    def _cookiecutter_boto3_uri(self, filename):
        """
        return a valid awscli URI pointing to the AWS S3 bucket root, preferaby via
        Cloudfront, otherwise via AWS S3 URI.
        examples
            filename:                           badge_classes/course_complete_badges/badge-icon-png-22.png
            settings.AWS_S3_CUSTOM_DOMAIN:      cdn.courses.smartlikefox.com
            settings.AWS_STORAGE_BUCKET_NAME:   smartlikefox-usa-prod-storage
            S3 URI:                             s3://smartlikefox-usa-prod-storage
        """

        try:
            # example: https://cdn.courses.smartlikefox.com/badge_classes/course_complete_badges/badge-icon-png-22.png
            aws_s3_custom_domain = "https://{aws_s3_custom_domain}/{filename}".format(
                aws_s3_custom_domain=settings.AWS_S3_CUSTOM_DOMAIN, filename=filename
            )
            validate = URLValidator()
            validate(aws_s3_custom_domain)
            return aws_s3_custom_domain
        except ValidationError:
            # example: s3://smartlikefox-usa-prod-storage/badge_classes/course_complete_badges/badge-icon-png-22.png
            aws_storage_bucket_name = "s3://{aws_storage_bucket_name}/{filename}".format(
                aws_storage_bucket_name=settings.AWS_STORAGE_BUCKET_NAME, filename=filename
            )
            return aws_storage_bucket_name

    def _create_badge(self, badge_class):
        """
        Create the badge class on Badgr.
        badge_class - badges.BadgeClass(models.Model)
        badge_class.image - models.ImageField(upload_to='badge_classes')
        """
        log.info("cookiecutter_plugin.badges.backends.badgr_boto3._create_badge() - start")
        if badge_class is None or badge_class.image is None:
            log.error("either received None for badge_class or badge_class.image is None")
            return

        image_filename = badge_class.image.name

        boto3_uri = self._cookiecutter_boto3_uri(image_filename)
        response = requests.get(boto3_uri)
        if response.status_code != requests.codes.ok:
            log.error(
                "received {status_code} response on URI {uri}".format(status_code=response.status_code, uri=boto3_uri)
            )
            response.raise_for_status()
            return

        # ---------------------------------------------------------------------
        # mcdaniel: everything following the http response is intended to match
        # the default badges backend exactly.
        # ---------------------------------------------------------------------

        image_content = response.content
        image_content_type = response.headers["content-type"]
        files = {"image": (image_filename, image_content, image_content_type)}
        data = {
            "name": badge_class.display_name,
            "criteriaUrl": badge_class.criteria,
            "description": badge_class.description,
        }
        result = requests.post(
            self._badge_create_url, headers=self._get_headers(), data=data, files=files, timeout=settings.BADGR_TIMEOUT
        )
        self._log_if_raised(result, data)
        try:
            result_json = result.json()
            badgr_badge_class = result_json["result"][0]
            badgr_server_slug = badgr_badge_class.get("entityId")
            badge_class.badgr_server_slug = badgr_server_slug
            badge_class.save()
        except Exception as excep:  # noqa: E902
            log.error(
                "Error on saving Badgr Server Slug of badge_class slug "
                '"{0}" with response json "{1}" : {2}'.format(badge_class.slug, result.json(), excep)
            )

        log.info("cookiecutter_plugin.badges.backends.badgr_boto3._create_badge() - finish")

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.