Merge pull request #33186 from thezaeemaanwar/remove_badges_app

[DEPR]: lms/djangoapps/badges
This commit is contained in:
Feanil Patel
2023-10-20 10:29:43 -04:00
committed by GitHub
72 changed files with 17 additions and 3860 deletions

View File

@@ -2,7 +2,6 @@
"lms-1": {
"settings": "lms.envs.test",
"paths": [
"lms/djangoapps/badges/",
"lms/djangoapps/branding/",
"lms/djangoapps/bulk_email/",
"lms/djangoapps/bulk_enroll/",

View File

@@ -126,11 +126,6 @@ class CourseMetadata:
exclude_list.append('enable_ccx')
exclude_list.append('ccx_connector')
# Do not show "Issue Open Badges" in Studio Advanced Settings
# if the feature is disabled.
if not settings.FEATURES.get('ENABLE_OPENBADGES'):
exclude_list.append('issue_badges')
# If the XBlockStudioConfiguration table is not being used, there is no need to
# display the "Allow Unsupported XBlocks" setting.
if not XBlockStudioConfigurationFlag.is_enabled():

View File

@@ -123,9 +123,6 @@ FEATURES['ENABLE_ENROLLMENT_TRACK_USER_PARTITION'] = True
# shown in Studio in a separate list.
FEATURES['ENABLE_SEPARATE_ARCHIVED_COURSES'] = True
# Enable support for OpenBadges accomplishments
FEATURES['ENABLE_OPENBADGES'] = True
# Enable partner support link in Studio footer
PARTNER_SUPPORT_EMAIL = 'partner-support@example.com'

View File

@@ -321,9 +321,6 @@ FEATURES = {
# Show video bumper in Studio
'ENABLE_VIDEO_BUMPER': False,
# Show issue open badges in Studio
'ENABLE_OPENBADGES': False,
# How many seconds to show the bumper again, default is 7 days:
'SHOW_BUMPER_PERIODICITY': 7 * 24 * 3600,

View File

@@ -3,6 +3,7 @@
from io import StringIO
import ddt
import unittest
from django.core.management import call_command
from django.db.transaction import TransactionManagementError, atomic
from django.test import TestCase, TransactionTestCase
@@ -120,6 +121,7 @@ class MigrationTests(TestCase):
Tests for migrations.
"""
@unittest.skip("Migration will delete several models. Need to ship not referencing it first")
@override_settings(MIGRATION_MODULES={})
def test_migrations_are_in_sync(self):
"""

View File

@@ -58,114 +58,6 @@ paths:
in: path
required: true
type: string
/badges/v1/assertions/user/{username}/:
get:
operationId: badges_v1_assertions_user_read
summary: '**Use Cases**'
description: |-
Request a list of assertions for a user, optionally constrained to a course.
**Example Requests**
GET /api/badges/v1/assertions/user/{username}/
**Response Values**
Body comprised of a list of objects with the following fields:
* badge_class: The badge class the assertion was awarded for. Represented as an object
with the following fields:
* slug: The identifier for the badge class
* issuing_component: The software component responsible for issuing this badge.
* display_name: The display name of the badge.
* course_id: The course key of the course this badge is scoped to, or null if it isn't scoped to a course.
* description: A description of the award and its significance.
* criteria: A description of what is needed to obtain this award.
* image_url: A URL to the icon image used to represent this award.
* image_url: The baked assertion image derived from the badge_class icon-- contains metadata about the award
in its headers.
* assertion_url: The URL to the OpenBadges BadgeAssertion object, for verification by compatible tools
and software.
**Params**
* slug (optional): The identifier for a particular badge class to filter by.
* issuing_component (optional): The issuing component for a particular badge class to filter by
(requires slug to have been specified, or this will be ignored.) If slug is provided and this is not,
assumes the issuing_component should be empty.
* course_id (optional): Returns assertions that were awarded as part of a particular course. If slug is
provided, and this field is not specified, assumes that the target badge has an empty course_id field.
'*' may be used to get all badges with the specified slug, issuing_component combination across all courses.
**Returns**
* 200 on success, with a list of Badge Assertion objects.
* 403 if a user who does not have permission to masquerade as
another user specifies a username other than their own.
* 404 if the specified user does not exist
{
"count": 7,
"previous": null,
"num_pages": 1,
"results": [
{
"badge_class": {
"slug": "special_award",
"issuing_component": "openedx__course",
"display_name": "Very Special Award",
"course_id": "course-v1:edX+DemoX+Demo_Course",
"description": "Awarded for people who did something incredibly special",
"criteria": "Do something incredibly special.",
"image": "http://example.com/media/badge_classes/badges/special_xdpqpBv_9FYOZwN.png"
},
"image_url": "http://badges.example.com/media/issued/cd75b69fc1c979fcc1697c8403da2bdf.png",
"assertion_url": "http://badges.example.com/public/assertions/07020647-e772-44dd-98b7-d13d34335ca6"
},
...
]
}
parameters:
- name: page
in: query
description: A page number within the paginated result set.
required: false
type: integer
- name: page_size
in: query
description: Number of results to return per page.
required: false
type: integer
responses:
'200':
description: ''
schema:
required:
- count
- results
type: object
properties:
count:
type: integer
next:
type: string
format: uri
x-nullable: true
previous:
type: string
format: uri
x-nullable: true
results:
type: array
items:
$ref: '#/definitions/BadgeAssertion'
tags:
- badges
parameters:
- name: username
in: path
required: true
type: string
/bookmarks/v1/bookmarks/:
get:
operationId: bookmarks_v1_bookmarks_list
@@ -9477,75 +9369,6 @@ paths:
required: true
type: string
definitions:
BadgeClass:
required:
- slug
- display_name
- description
- criteria
type: object
properties:
slug:
title: Slug
type: string
format: slug
pattern: ^[-a-zA-Z0-9_]+$
maxLength: 255
minLength: 1
issuing_component:
title: Issuing component
type: string
format: slug
pattern: ^[-a-zA-Z0-9_]+$
default: ''
maxLength: 50
display_name:
title: Display name
type: string
maxLength: 255
minLength: 1
course_id:
title: Course id
type: string
maxLength: 255
description:
title: Description
type: string
minLength: 1
criteria:
title: Criteria
type: string
minLength: 1
image_url:
title: Image url
type: string
readOnly: true
format: uri
BadgeAssertion:
required:
- image_url
- assertion_url
type: object
properties:
badge_class:
$ref: '#/definitions/BadgeClass'
image_url:
title: Image url
type: string
format: uri
maxLength: 200
minLength: 1
assertion_url:
title: Assertion url
type: string
format: uri
maxLength: 200
minLength: 1
created:
title: Created
type: string
format: date-time
readOnly: true
CCXCourse:
required:
- master_course_id

View File

@@ -9,7 +9,6 @@ Studio.
:maxdepth: 2
lms/modules
lms/djangoapps/badges/modules
lms/djangoapps/branding/modules
lms/djangoapps/bulk_email/modules
lms/djangoapps/courseware/modules

View File

@@ -1,20 +0,0 @@
"""
Admin registration for Badge Models
"""
from config_models.admin import ConfigurationModelAdmin
from django.contrib import admin
from lms.djangoapps.badges.models import (
BadgeAssertion,
BadgeClass,
CourseCompleteImageConfiguration,
CourseEventBadgesConfiguration
)
admin.site.register(CourseCompleteImageConfiguration)
admin.site.register(BadgeClass)
admin.site.register(BadgeAssertion)
# Use the standard Configuration Model Admin handler for this model.
admin.site.register(CourseEventBadgesConfiguration, ConfigurationModelAdmin)

View File

@@ -1,30 +0,0 @@
"""
Serializers for Badges
"""
from rest_framework import serializers
from lms.djangoapps.badges.models import BadgeAssertion, BadgeClass
class BadgeClassSerializer(serializers.ModelSerializer):
"""
Serializer for BadgeClass model.
"""
image_url = serializers.ImageField(source='image')
class Meta:
model = BadgeClass
fields = ('slug', 'issuing_component', 'display_name', 'course_id', 'description', 'criteria', 'image_url')
class BadgeAssertionSerializer(serializers.ModelSerializer):
"""
Serializer for the BadgeAssertion model.
"""
badge_class = BadgeClassSerializer(read_only=True)
class Meta:
model = BadgeAssertion
fields = ('badge_class', 'image_url', 'assertion_url', 'created')

View File

@@ -1,218 +0,0 @@
"""
Tests for the badges API views.
"""
from ddt import data, ddt, unpack
from django.conf import settings
from django.test.utils import override_settings
from common.djangoapps.student.tests.factories import UserFactory
from common.djangoapps.util.testing import UrlResetMixin
from lms.djangoapps.badges.tests.factories import BadgeAssertionFactory, BadgeClassFactory, RandomBadgeClassFactory
from openedx.core.lib.api.test_utils import ApiTestCase
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
FEATURES_WITH_BADGES_ENABLED = settings.FEATURES.copy()
FEATURES_WITH_BADGES_ENABLED['ENABLE_OPENBADGES'] = True
@override_settings(FEATURES=FEATURES_WITH_BADGES_ENABLED)
class UserAssertionTestCase(UrlResetMixin, ModuleStoreTestCase, ApiTestCase):
"""
Mixin for badge API tests.
"""
def setUp(self):
super().setUp()
self.course = CourseFactory.create()
self.user = UserFactory.create()
# Password defined by factory.
self.client.login(username=self.user.username, password=self.TEST_PASSWORD)
def url(self):
"""
Return the URL to look up the current user's assertions.
"""
return f'/api/badges/v1/assertions/user/{self.user.username}/'
def check_class_structure(self, badge_class, json_class):
"""
Check a JSON response against a known badge class.
"""
assert badge_class.issuing_component == json_class['issuing_component']
assert badge_class.slug == json_class['slug']
assert badge_class.image.url in json_class['image_url']
assert badge_class.description == json_class['description']
assert badge_class.criteria == json_class['criteria']
assert (badge_class.course_id and str(badge_class.course_id)) == json_class['course_id']
def check_assertion_structure(self, assertion, json_assertion):
"""
Check a JSON response against a known assertion object.
"""
assert assertion.image_url == json_assertion['image_url']
assert assertion.assertion_url == json_assertion['assertion_url']
self.check_class_structure(assertion.badge_class, json_assertion['badge_class'])
def get_course_id(self, wildcard, badge_class):
"""
Used for tests which may need to test for a course_id or a wildcard.
"""
if wildcard:
return '*'
else:
return str(badge_class.course_id)
def create_badge_class(self, check_course, **kwargs):
"""
Create a badge class, using a course id if it's relevant to the URL pattern.
"""
if check_course:
return RandomBadgeClassFactory.create(course_id=self.course.location.course_key, **kwargs)
return RandomBadgeClassFactory.create(**kwargs)
def get_qs_args(self, check_course, wildcard, badge_class):
"""
Get a dictionary to be serialized into querystring params based on class settings.
"""
qs_args = {
'issuing_component': badge_class.issuing_component,
'slug': badge_class.slug,
}
if check_course:
qs_args['course_id'] = self.get_course_id(wildcard, badge_class)
return qs_args
class TestUserBadgeAssertions(UserAssertionTestCase):
"""
Test the general badge assertions retrieval view.
"""
def test_get_assertions(self):
"""
Verify we can get all of a user's badge assertions.
"""
for dummy in range(3):
BadgeAssertionFactory(user=self.user)
# Add in a course scoped badge-- these should not be excluded from the full listing.
BadgeAssertionFactory(user=self.user, badge_class=BadgeClassFactory(course_id=self.course.location.course_key))
# Should not be included.
for dummy in range(3):
self.create_badge_class(False)
response = self.get_json(self.url())
assert len(response['results']) == 4
def test_assertion_structure(self):
badge_class = self.create_badge_class(False)
assertion = BadgeAssertionFactory.create(user=self.user, badge_class=badge_class)
response = self.get_json(self.url())
self.check_assertion_structure(assertion, response['results'][0])
class TestUserCourseBadgeAssertions(UserAssertionTestCase):
"""
Test the Badge Assertions view with the course_id filter.
"""
def test_get_assertions(self):
"""
Verify we can get assertions via the course_id and username.
"""
course_key = self.course.location.course_key
badge_class = BadgeClassFactory.create(course_id=course_key)
for dummy in range(3):
BadgeAssertionFactory.create(user=self.user, badge_class=badge_class)
# Should not be included, as they don't share the target badge class.
for dummy in range(3):
BadgeAssertionFactory.create(user=self.user)
# Also should not be included, as they don't share the same user.
for dummy in range(6):
BadgeAssertionFactory.create(badge_class=badge_class)
response = self.get_json(self.url(), data={'course_id': str(course_key)})
assert len(response['results']) == 3
unused_course = CourseFactory.create()
response = self.get_json(self.url(), data={'course_id': str(unused_course.location.course_key)})
assert len(response['results']) == 0
def test_assertion_structure(self):
"""
Verify the badge assertion structure is as expected when a course is involved.
"""
course_key = self.course.location.course_key
badge_class = BadgeClassFactory.create(course_id=course_key)
assertion = BadgeAssertionFactory.create(badge_class=badge_class, user=self.user)
response = self.get_json(self.url())
self.check_assertion_structure(assertion, response['results'][0])
@ddt
class TestUserBadgeAssertionsByClass(UserAssertionTestCase):
"""
Test the Badge Assertions view with the badge class filter.
"""
@unpack
@data((False, False), (True, False), (True, True))
def test_get_assertions(self, check_course, wildcard):
"""
Verify we can get assertions via the badge class and username.
"""
badge_class = self.create_badge_class(check_course)
for dummy in range(3):
BadgeAssertionFactory.create(user=self.user, badge_class=badge_class)
if badge_class.course_id:
# Also create a version of this badge under a different course.
alt_class = BadgeClassFactory.create(
slug=badge_class.slug, issuing_component=badge_class.issuing_component,
course_id=CourseFactory.create().location.course_key
)
BadgeAssertionFactory.create(user=self.user, badge_class=alt_class)
# Same badge class, but different user. Should not show up in the list.
for dummy in range(5):
BadgeAssertionFactory.create(badge_class=badge_class)
# Different badge class AND different user. Certainly shouldn't show up in the list!
for dummy in range(6):
BadgeAssertionFactory.create()
response = self.get_json(
self.url(),
data=self.get_qs_args(check_course, wildcard, badge_class),
)
if wildcard:
expected_length = 4
else:
expected_length = 3
assert len(response['results']) == expected_length
unused_class = self.create_badge_class(check_course, slug='unused_slug', issuing_component='unused_component')
response = self.get_json(
self.url(),
data=self.get_qs_args(check_course, wildcard, unused_class),
)
assert len(response['results']) == 0
def check_badge_class_assertion(self, check_course, wildcard, badge_class):
"""
Given a badge class, create an assertion for the current user and fetch it, checking the structure.
"""
assertion = BadgeAssertionFactory.create(badge_class=badge_class, user=self.user)
response = self.get_json(
self.url(),
data=self.get_qs_args(check_course, wildcard, badge_class),
)
self.check_assertion_structure(assertion, response['results'][0])
@unpack
@data((False, False), (True, False), (True, True))
def test_assertion_structure(self, check_course, wildcard):
self.check_badge_class_assertion(check_course, wildcard, self.create_badge_class(check_course))
@unpack
@data((False, False), (True, False), (True, True))
def test_empty_issuing_component(self, check_course, wildcard):
self.check_badge_class_assertion(
check_course, wildcard, self.create_badge_class(check_course, issuing_component='')
)

View File

@@ -1,14 +0,0 @@
"""
URLs for badges API
"""
from django.conf import settings
from django.urls import re_path
from .views import UserBadgeAssertions
urlpatterns = [
re_path('^assertions/user/' + settings.USERNAME_PATTERN + '/$',
UserBadgeAssertions.as_view(), name='user_assertions'),
]

View File

@@ -1,139 +0,0 @@
"""
API views for badges
"""
from edx_rest_framework_extensions.auth.session.authentication import \
SessionAuthenticationAllowInactiveUser
from opaque_keys import InvalidKeyError
from opaque_keys.edx.django.models import CourseKeyField
from opaque_keys.edx.keys import CourseKey
from rest_framework import generics
from rest_framework.exceptions import APIException
from lms.djangoapps.badges.models import BadgeAssertion
from openedx.core.djangoapps.user_api.permissions import is_field_shared_factory
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
from .serializers import BadgeAssertionSerializer
class InvalidCourseKeyError(APIException):
"""
Raised the course key given isn't valid.
"""
status_code = 400
default_detail = "The course key provided was invalid."
class UserBadgeAssertions(generics.ListAPIView):
"""
**Use Cases**
Request a list of assertions for a user, optionally constrained to a course.
**Example Requests**
GET /api/badges/v1/assertions/user/{username}/
**Response Values**
Body comprised of a list of objects with the following fields:
* badge_class: The badge class the assertion was awarded for. Represented as an object
with the following fields:
* slug: The identifier for the badge class
* issuing_component: The software component responsible for issuing this badge.
* display_name: The display name of the badge.
* course_id: The course key of the course this badge is scoped to, or null if it isn't scoped to a course.
* description: A description of the award and its significance.
* criteria: A description of what is needed to obtain this award.
* image_url: A URL to the icon image used to represent this award.
* image_url: The baked assertion image derived from the badge_class icon-- contains metadata about the award
in its headers.
* assertion_url: The URL to the OpenBadges BadgeAssertion object, for verification by compatible tools
and software.
**Params**
* slug (optional): The identifier for a particular badge class to filter by.
* issuing_component (optional): The issuing component for a particular badge class to filter by
(requires slug to have been specified, or this will be ignored.) If slug is provided and this is not,
assumes the issuing_component should be empty.
* course_id (optional): Returns assertions that were awarded as part of a particular course. If slug is
provided, and this field is not specified, assumes that the target badge has an empty course_id field.
'*' may be used to get all badges with the specified slug, issuing_component combination across all courses.
**Returns**
* 200 on success, with a list of Badge Assertion objects.
* 403 if a user who does not have permission to masquerade as
another user specifies a username other than their own.
* 404 if the specified user does not exist
{
"count": 7,
"previous": null,
"num_pages": 1,
"results": [
{
"badge_class": {
"slug": "special_award",
"issuing_component": "openedx__course",
"display_name": "Very Special Award",
"course_id": "course-v1:edX+DemoX+Demo_Course",
"description": "Awarded for people who did something incredibly special",
"criteria": "Do something incredibly special.",
"image": "http://example.com/media/badge_classes/badges/special_xdpqpBv_9FYOZwN.png"
},
"image_url": "http://badges.example.com/media/issued/cd75b69fc1c979fcc1697c8403da2bdf.png",
"assertion_url": "http://badges.example.com/public/assertions/07020647-e772-44dd-98b7-d13d34335ca6"
},
...
]
}
"""
serializer_class = BadgeAssertionSerializer
authentication_classes = (
BearerAuthenticationAllowInactiveUser,
SessionAuthenticationAllowInactiveUser
)
permission_classes = (is_field_shared_factory("accomplishments_shared"),)
def filter_queryset(self, queryset):
"""
Return most recent to least recent badge.
"""
return queryset.order_by('-created')
def get_queryset(self):
"""
Get all badges for the username specified.
"""
queryset = BadgeAssertion.objects.filter(user__username=self.kwargs['username'])
provided_course_id = self.request.query_params.get('course_id')
if provided_course_id == '*':
# We might want to get all the matching course scoped badges to see how many courses
# a user managed to get a specific award on.
course_id = None
elif provided_course_id:
try:
course_id = CourseKey.from_string(provided_course_id)
except InvalidKeyError:
raise InvalidCourseKeyError # lint-amnesty, pylint: disable=raise-missing-from
elif 'slug' not in self.request.query_params:
# Need to get all badges for the user.
course_id = None
else:
# Django won't let us use 'None' for querying a ForeignKey field. We have to use this special
# 'Empty' value to indicate we're looking only for badges without a course key set.
course_id = CourseKeyField.Empty
if course_id is not None:
queryset = queryset.filter(badge_class__course_id=course_id)
if self.request.query_params.get('slug'):
queryset = queryset.filter(
badge_class__slug=self.request.query_params['slug'],
badge_class__issuing_component=self.request.query_params.get('issuing_component', '')
)
return queryset

View File

@@ -13,9 +13,3 @@ class BadgesConfig(AppConfig):
Application Configuration for Badges.
"""
name = 'lms.djangoapps.badges'
def ready(self):
"""
Connect signal handlers.
"""
from . import handlers # pylint: disable=unused-import

View File

@@ -1,324 +0,0 @@
"""
Badge Awarding backend for Badgr-Server.
"""
import base64
import datetime
import json
import hashlib
import logging
import mimetypes
import requests
from cryptography.fernet import Fernet
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from eventtracking import tracker
from lazy import lazy # lint-amnesty, pylint: disable=no-name-in-module
from requests.packages.urllib3.exceptions import HTTPError # lint-amnesty, pylint: disable=import-error
from edx_django_utils.cache import TieredCache
from lms.djangoapps.badges.backends.base import BadgeBackend
from lms.djangoapps.badges.models import BadgeAssertion
MAX_SLUG_LENGTH = 255
LOGGER = logging.getLogger(__name__)
class BadgrBackend(BadgeBackend):
"""
Backend for Badgr-Server by Concentric Sky. http://info.badgr.io/
"""
badges = []
def __init__(self):
super().__init__()
if None in (settings.BADGR_USERNAME,
settings.BADGR_PASSWORD,
settings.BADGR_TOKENS_CACHE_KEY,
settings.BADGR_ISSUER_SLUG,
settings.BADGR_BASE_URL):
error_msg = (
"One or more of the required settings are not defined. "
"Required settings: BADGR_USERNAME, BADGR_PASSWORD, "
"BADGR_TOKENS_CACHE_KEY, BADGR_ISSUER_SLUG, BADGR_BASE_URL.")
LOGGER.error(error_msg)
raise ImproperlyConfigured(error_msg)
@lazy
def _base_url(self):
"""
Base URL for API requests that contain the issuer slug.
"""
return f"{settings.BADGR_BASE_URL}/v2/issuers/{settings.BADGR_ISSUER_SLUG}"
@lazy
def _badge_create_url(self):
"""
URL for generating a new Badge specification
"""
return f"{self._base_url}/badgeclasses"
def _badge_url(self, slug):
"""
Get the URL for a course's badge in a given mode.
"""
return f"{settings.BADGR_BASE_URL}/v2/badgeclasses/{slug}"
def _assertion_url(self, slug):
"""
URL for generating a new assertion.
"""
return f"{self._badge_url(slug)}/assertions"
def _slugify(self, badge_class):
"""
Get a compatible badge slug from the specification.
"""
slug = badge_class.issuing_component + badge_class.slug
if badge_class.issuing_component and badge_class.course_id:
# Make this unique to the course, and down to 64 characters.
# We don't do this to badges without issuing_component set for backwards compatibility.
slug = hashlib.sha256((slug + str(badge_class.course_id)).encode('utf-8')).hexdigest()
if len(slug) > MAX_SLUG_LENGTH:
# Will be 64 characters.
slug = hashlib.sha256(slug).hexdigest()
return slug
def _log_if_raised(self, response, data):
"""
Log server response if there was an error.
"""
try:
response.raise_for_status()
except HTTPError:
LOGGER.error(
"Encountered an error when contacting the Badgr-Server. Request sent to %r with headers %r.\n"
"and data values %r\n"
"Response status was %s.\n%s",
response.request.url, response.request.headers,
data,
response.status_code, response.content
)
raise
def _create_badge(self, badge_class):
"""
Create the badge class on Badgr.
"""
image = badge_class.image
# We don't want to bother validating the file any further than making sure we can detect its MIME type,
# for HTTP. The Badgr-Server should tell us if there's anything in particular wrong with it.
content_type, __ = mimetypes.guess_type(image.name)
if not content_type:
raise ValueError(
"Could not determine content-type of image! Make sure it is a properly named .png file. "
"Filename was: {}".format(image.name)
)
with open(image.path, 'rb') as image_file:
files = {'image': (image.name, image_file, 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: # pylint: disable=broad-except
LOGGER.error(
'Error on saving Badgr Server Slug of badge_class slug '
'"{0}" with response json "{1}" : {2}'.format(
badge_class.slug, result.json(), excep))
def _send_assertion_created_event(self, user, assertion):
"""
Send an analytics event to record the creation of a badge assertion.
"""
tracker.emit(
'edx.badge.assertion.created', {
'user_id': user.id,
'badge_slug': assertion.badge_class.slug,
'badge_badgr_server_slug': assertion.badge_class.badgr_server_slug,
'badge_name': assertion.badge_class.display_name,
'issuing_component': assertion.badge_class.issuing_component,
'course_id': str(assertion.badge_class.course_id),
'enrollment_mode': assertion.badge_class.mode,
'assertion_id': assertion.id,
'assertion_image_url': assertion.image_url,
'assertion_json_url': assertion.assertion_url,
'issuer': assertion.data.get('issuer'),
}
)
def _create_assertion(self, badge_class, user, evidence_url):
"""
Register an assertion with the Badgr server for a particular user for a specific class.
"""
data = {
"recipient": {
"identity": user.email,
"type": "email"
},
"evidence": [
{
"url": evidence_url
}
],
"notify": settings.BADGR_ENABLE_NOTIFICATIONS,
}
response = requests.post(
self._assertion_url(badge_class.badgr_server_slug),
headers=self._get_headers(),
json=data,
timeout=settings.BADGR_TIMEOUT
)
self._log_if_raised(response, data)
assertion, __ = BadgeAssertion.objects.get_or_create(user=user, badge_class=badge_class)
try:
response_json = response.json()
assertion.data = response_json['result'][0]
assertion.image_url = assertion.data['image']
assertion.assertion_url = assertion.data['openBadgeId']
assertion.backend = 'BadgrBackend'
assertion.save()
self._send_assertion_created_event(user, assertion)
return assertion
except Exception as exc: # pylint: disable=broad-except
LOGGER.error(
'Error saving BadgeAssertion for user: "{0}" '
'with response from server: {1};'
'Encountered exception: {2}'.format(
user.email, response.text, exc))
@staticmethod
def _fernet_setup():
"""
Set up the Fernet class for encrypting/decrypting tokens.
Fernet keys must always be URL-safe base64 encoded 32-byte binary
strings. Use the SECRET_KEY for creating the encryption key.
"""
fernet_key = base64.urlsafe_b64encode(
settings.SECRET_KEY.ljust(64).encode('utf-8')[:32]
)
return Fernet(fernet_key)
def _encrypt_token(self, token):
"""
Encrypt a token
"""
fernet = self._fernet_setup()
return fernet.encrypt(token.encode('utf-8'))
def _decrypt_token(self, token):
"""
Decrypt a token
"""
fernet = self._fernet_setup()
return fernet.decrypt(token).decode()
def _get_and_cache_oauth_tokens(self, refresh_token=None):
"""
Get or renew OAuth tokens. If a refresh_token is provided,
use it to renew tokens, otherwise create new ones.
Once tokens are created/renewed, encrypt the values and cache them.
"""
data = {
'username': settings.BADGR_USERNAME,
'password': settings.BADGR_PASSWORD,
}
if refresh_token:
data = {
'grant_type': 'refresh_token',
'refresh_token': refresh_token
}
oauth_url = "{}/o/token".format(settings.BADGR_BASE_URL)
response = requests.post(
oauth_url, data=data, timeout=settings.BADGR_TIMEOUT
)
self._log_if_raised(response, data)
try:
data = response.json()
result = {
'access_token': self._encrypt_token(data['access_token']),
'refresh_token': self._encrypt_token(data['refresh_token']),
'expires_at': datetime.datetime.utcnow() + datetime.timedelta(
seconds=data['expires_in'])
}
# The refresh_token is long-lived, we want to be able to retrieve
# it from cache as long as possible.
# Set the cache timeout to None so the cache key never expires
# (https://docs.djangoproject.com/en/2.2/topics/cache/#cache-arguments)
TieredCache.set_all_tiers(
settings.BADGR_TOKENS_CACHE_KEY, result, None)
return result
except (KeyError, json.decoder.JSONDecodeError) as json_error:
raise requests.RequestException(response=response) from json_error
def _get_access_token(self):
"""
Get an access token from cache if one is present and valid. If a
token is cached but expired, renew it. If all fails or a token has
not yet been cached, create a new one.
"""
tokens = {}
cached_response = TieredCache.get_cached_response(
settings.BADGR_TOKENS_CACHE_KEY)
if cached_response.is_found:
cached_tokens = cached_response.value
# add a 5 seconds buffer to the cutoff timestamp to make sure
# the token will not expire while in use
expiry_cutoff = (
datetime.datetime.utcnow() + datetime.timedelta(seconds=5))
if cached_tokens.get('expires_at') > expiry_cutoff:
tokens = cached_tokens
else:
# renew the tokens with the cached `refresh_token`
refresh_token = self._decrypt_token(cached_tokens.get(
'refresh_token'))
tokens = self._get_and_cache_oauth_tokens(
refresh_token=refresh_token)
# if no tokens are cached or something went wrong with
# retreiving/renewing them, go and create new tokens
if not tokens:
tokens = self._get_and_cache_oauth_tokens()
return self._decrypt_token(tokens.get('access_token'))
def _get_headers(self):
"""
Headers to send along with the request-- used for authentication.
"""
access_token = self._get_access_token()
return {'Authorization': 'Bearer {}'.format(access_token)}
def _ensure_badge_created(self, badge_class):
"""
Verify a badge has been created for this badge class, and create it if not.
"""
slug = badge_class.badgr_server_slug
if slug in BadgrBackend.badges:
return
response = requests.get(self._badge_url(slug), headers=self._get_headers(), timeout=settings.BADGR_TIMEOUT)
if response.status_code != 200:
self._create_badge(badge_class)
BadgrBackend.badges.append(slug)
def award(self, badge_class, user, evidence_url=None):
"""
Make sure the badge class has been created on the backend, and then award the badge class to the user.
"""
self._ensure_badge_created(badge_class)
return self._create_assertion(badge_class, user, evidence_url)

View File

@@ -1,18 +0,0 @@
"""
Base class for badge backends.
"""
from abc import ABCMeta, abstractmethod
class BadgeBackend(metaclass=ABCMeta):
"""
Defines the interface for badging backends.
"""
@abstractmethod
def award(self, badge_class, user, evidence_url=None):
"""
Create a badge assertion for the user using this backend.
"""

View File

@@ -1,15 +0,0 @@
"""
Dummy backend, for use in testing.
"""
from lms.djangoapps.badges.backends.base import BadgeBackend
from lms.djangoapps.badges.tests.factories import BadgeAssertionFactory
class DummyBackend(BadgeBackend):
"""
Dummy backend that creates assertions without contacting any real-world backend.
"""
def award(self, badge_class, user, evidence_url=None):
return BadgeAssertionFactory(badge_class=badge_class, user=user)

View File

@@ -1,301 +0,0 @@
"""
Tests for BadgrBackend
"""
import datetime
from unittest.mock import Mock, call, patch
import json
import ddt
import httpretty
from django.test.utils import override_settings
from lazy.lazy import lazy # lint-amnesty, pylint: disable=no-name-in-module
from edx_django_utils.cache import TieredCache
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
from common.djangoapps.track.tests import FROZEN_TIME, EventTrackingTestCase
from lms.djangoapps.badges.backends.badgr import BadgrBackend
from lms.djangoapps.badges.models import BadgeAssertion
from lms.djangoapps.badges.tests.factories import BadgeClassFactory
from openedx.core.lib.tests.assertions.events import assert_event_matches
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
BADGR_SETTINGS = {
'BADGR_BASE_URL': 'https://example.com',
'BADGR_ISSUER_SLUG': 'test-issuer',
'BADGR_USERNAME': 'example@example.com',
'BADGR_PASSWORD': 'password',
'BADGR_TOKENS_CACHE_KEY': 'badgr-test-cache-key'
}
# Should be the hashed result of test_slug as the slug, and test_component as the component
EXAMPLE_SLUG = '9e915d55bb304a73d20c453531d3c27f81574218413c23903823d20d11b587ae'
BADGR_SERVER_SLUG = 'test_badgr_server_slug'
# pylint: disable=protected-access
@ddt.ddt
@override_settings(**BADGR_SETTINGS)
@httpretty.activate
class BadgrBackendTestCase(ModuleStoreTestCase, EventTrackingTestCase):
"""
Tests the BadgeHandler object
"""
def setUp(self):
"""
Create a course and user to test with.
"""
super().setUp()
# Need key to be deterministic to test slugs.
self.course = CourseFactory.create(
org='edX', course='course_test', run='test_run', display_name='Badged',
start=datetime.datetime(year=2015, month=5, day=19),
end=datetime.datetime(year=2015, month=5, day=20)
)
self.user = UserFactory.create(email='example@example.com')
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.location.course_key, mode='honor')
# Need to empty this on each run.
BadgrBackend.badges = []
self.badge_class = BadgeClassFactory.create(course_id=self.course.location.course_key)
self.legacy_badge_class = BadgeClassFactory.create(
course_id=self.course.location.course_key, issuing_component=''
)
self.no_course_badge_class = BadgeClassFactory.create()
TieredCache.dangerous_clear_all_tiers()
httpretty.httpretty.reset()
@lazy
def handler(self):
"""
Lazily loads a BadgeHandler object for the current course. Can't do this on setUp because the settings
overrides aren't in place.
"""
return BadgrBackend()
def _mock_badgr_tokens_api(self, result):
assert httpretty.is_enabled()
responses = [httpretty.Response(body=json.dumps(result),
content_type='application/json')]
httpretty.register_uri(httpretty.POST,
'https://example.com/o/token',
responses=responses)
def test_urls(self):
"""
Make sure the handler generates the correct URLs for different API tasks.
"""
assert self.handler._base_url == 'https://example.com/v2/issuers/test-issuer'
# lint-amnesty, pylint: disable=no-member
assert self.handler._badge_create_url == 'https://example.com/v2/issuers/test-issuer/badgeclasses'
# lint-amnesty, pylint: disable=no-member
assert self.handler._badge_url('test_slug_here') ==\
'https://example.com/v2/badgeclasses/test_slug_here'
assert self.handler._assertion_url('another_test_slug') ==\
'https://example.com/v2/badgeclasses/another_test_slug/assertions'
def check_headers(self, headers):
"""
Verify the a headers dict from a requests call matches the proper auth info.
"""
assert headers == {'Authorization': 'Bearer 12345'}
def test_get_headers(self):
"""
Check to make sure the handler generates appropriate HTTP headers.
"""
self.handler._get_access_token = Mock(return_value='12345')
self.check_headers(self.handler._get_headers()) # lint-amnesty, pylint: disable=no-member
@patch('requests.post')
def test_create_badge(self, post):
"""
Verify badge spec creation works.
"""
self.handler._get_access_token = Mock(return_value='12345')
with self.allow_transaction_exception():
self.handler._create_badge(self.badge_class)
args, kwargs = post.call_args
assert args[0] == 'https://example.com/v2/issuers/test-issuer/badgeclasses'
assert kwargs['files']['image'][0] == self.badge_class.image.name
assert kwargs['files']['image'][2] == 'image/png'
self.check_headers(kwargs['headers'])
assert kwargs['data'] ==\
{'name': 'Test Badge',
'criteriaUrl': 'https://example.com/syllabus',
'description': "Yay! It's a test badge."}
def test_ensure_badge_created_cache(self):
"""
Make sure ensure_badge_created doesn't call create_badge if we know the badge is already there.
"""
BadgrBackend.badges.append(BADGR_SERVER_SLUG)
self.handler._create_badge = Mock()
self.handler._ensure_badge_created(self.badge_class) # lint-amnesty, pylint: disable=no-member
assert not self.handler._create_badge.called
@ddt.unpack
@ddt.data(
('badge_class', EXAMPLE_SLUG),
('legacy_badge_class', 'test_slug'),
('no_course_badge_class', 'test_componenttest_slug')
)
def test_slugs(self, badge_class_type, slug):
assert self.handler._slugify(getattr(self, badge_class_type)) == slug
# lint-amnesty, pylint: disable=no-member
@patch('requests.get')
def test_ensure_badge_created_checks(self, get):
response = Mock()
response.status_code = 200
get.return_value = response
assert 'test_componenttest_slug' not in BadgrBackend.badges
self.handler._get_access_token = Mock(return_value='12345')
self.handler._create_badge = Mock()
self.handler._ensure_badge_created(self.badge_class) # lint-amnesty, pylint: disable=no-member
assert get.called
args, kwargs = get.call_args
assert args[0] == (
'https://example.com/v2/badgeclasses/' +
BADGR_SERVER_SLUG)
self.check_headers(kwargs['headers'])
assert BADGR_SERVER_SLUG in BadgrBackend.badges
assert not self.handler._create_badge.called
@patch('requests.get')
def test_ensure_badge_created_creates(self, get):
response = Mock()
response.status_code = 404
get.return_value = response
assert BADGR_SERVER_SLUG not in BadgrBackend.badges
self.handler._get_access_token = Mock(return_value='12345')
self.handler._create_badge = Mock()
self.handler._ensure_badge_created(self.badge_class) # lint-amnesty, pylint: disable=no-member
assert self.handler._create_badge.called
assert self.handler._create_badge.call_args == call(self.badge_class)
assert BADGR_SERVER_SLUG in BadgrBackend.badges
@patch('requests.post')
def test_badge_creation_event(self, post):
result = {
'result': [{
'openBadgeId': 'http://www.example.com/example',
'image': 'http://www.example.com/example.png',
'issuer': 'https://example.com/v2/issuers/test-issuer'
}]
}
response = Mock()
response.json.return_value = result
post.return_value = response
self.recreate_tracker()
self.handler._get_access_token = Mock(return_value='12345')
self.handler._create_assertion(self.badge_class, self.user, 'https://example.com/irrefutable_proof') # lint-amnesty, pylint: disable=no-member
args, kwargs = post.call_args
assert args[0] == ((
'https://example.com/v2/badgeclasses/' +
BADGR_SERVER_SLUG) +
'/assertions')
self.check_headers(kwargs['headers'])
assertion = BadgeAssertion.objects.get(user=self.user, badge_class__course_id=self.course.location.course_key)
assert assertion.data == result['result'][0]
assert assertion.image_url == 'http://www.example.com/example.png'
assert assertion.assertion_url == 'http://www.example.com/example'
assert kwargs['json'] == {"recipient": {"identity": 'example@example.com', "type": "email"},
"evidence": [{"url": 'https://example.com/irrefutable_proof'}],
"notify": False}
assert_event_matches({
'name': 'edx.badge.assertion.created',
'data': {
'user_id': self.user.id,
'course_id': str(self.course.location.course_key),
'enrollment_mode': 'honor',
'assertion_id': assertion.id,
'badge_name': 'Test Badge',
'badge_slug': 'test_slug',
'badge_badgr_server_slug': BADGR_SERVER_SLUG,
'issuing_component': 'test_component',
'assertion_image_url': 'http://www.example.com/example.png',
'assertion_json_url': 'http://www.example.com/example',
'issuer': 'https://example.com/v2/issuers/test-issuer',
},
'context': {},
'timestamp': FROZEN_TIME
}, self.get_event())
def test_get_new_tokens(self):
result = {
'access_token': '12345',
'refresh_token': '67890',
'expires_in': 86400,
}
self._mock_badgr_tokens_api(result)
self.handler._get_and_cache_oauth_tokens()
assert 'o/token' in httpretty.httpretty.last_request.path
assert httpretty.httpretty.last_request.parsed_body == {
'username': ['example@example.com'],
'password': ['password']}
def test_renew_tokens(self):
result = {
'access_token': '12345',
'refresh_token': '67890',
'expires_in': 86400,
}
self._mock_badgr_tokens_api(result)
self.handler._get_and_cache_oauth_tokens(refresh_token='67890')
assert 'o/token' in httpretty.httpretty.last_request.path
assert httpretty.httpretty.last_request.parsed_body == {
'grant_type': ['refresh_token'],
'refresh_token': ['67890']}
def test_get_access_token_from_cache_valid(self):
encrypted_access_token = self.handler._encrypt_token('12345')
encrypted_refresh_token = self.handler._encrypt_token('67890')
tokens = {
'access_token': encrypted_access_token,
'refresh_token': encrypted_refresh_token,
'expires_at': datetime.datetime.utcnow() + datetime.timedelta(seconds=20)
}
TieredCache.set_all_tiers('badgr-test-cache-key', tokens, None)
access_token = self.handler._get_access_token()
assert access_token == self.handler._decrypt_token(
tokens.get('access_token'))
def test_get_access_token_from_cache_expired(self):
encrypted_access_token = self.handler._encrypt_token('12345')
encrypted_refresh_token = self.handler._encrypt_token('67890')
tokens = {
'access_token': encrypted_access_token,
'refresh_token': encrypted_refresh_token,
'expires_at': datetime.datetime.utcnow()
}
TieredCache.set_all_tiers('badgr-test-cache-key', tokens, None)
result = {
'access_token': '12345',
'refresh_token': '67890',
'expires_in': 86400,
}
self._mock_badgr_tokens_api(result)
access_token = self.handler._get_access_token()
assert access_token == result.get('access_token')
assert 'o/token' in httpretty.httpretty.last_request.path
assert httpretty.httpretty.last_request.parsed_body == {
'grant_type': ['refresh_token'],
'refresh_token': [self.handler._decrypt_token(
tokens.get('refresh_token'))]}
def test_get_access_token_from_cache_none(self):
result = {
'access_token': '12345',
'refresh_token': '67890',
'expires_in': 86400,
}
self._mock_badgr_tokens_api(result)
access_token = self.handler._get_access_token()
assert access_token == result.get('access_token')
assert 'o/token' in httpretty.httpretty.last_request.path
assert httpretty.httpretty.last_request.parsed_body == {
'username': ['example@example.com'],
'password': ['password']}

View File

@@ -6,14 +6,9 @@ Helper functions for the course complete event that was originally included with
import hashlib
import logging
from django.urls import reverse
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from lms.djangoapps.badges.models import BadgeAssertion, BadgeClass, CourseCompleteImageConfiguration
from lms.djangoapps.badges.utils import requires_badges_enabled, site_prefix
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
LOGGER = logging.getLogger(__name__)
@@ -25,9 +20,7 @@ def course_slug(course_key, mode):
"""
Legacy: Not to be used as a model for constructing badge slugs. Included for compatibility with the original badge
type, awarded on course completion.
Slug ought to be deterministic and limited in size so it's not too big for Badgr.
Badgr's max slug length is 255.
"""
# Seven digits should be enough to realistically avoid collisions. That's what git services use.
@@ -65,65 +58,4 @@ def evidence_url(user_id, course_key):
Generates a URL to the user's Certificate HTML view, along with a GET variable that will signal the evidence visit
event.
"""
course_id = str(course_key)
# avoid circular import problems
from lms.djangoapps.certificates.models import GeneratedCertificate
cert = GeneratedCertificate.eligible_certificates.get(user__id=int(user_id), course_id=course_id)
return site_prefix() + reverse(
'certificates:render_cert_by_uuid', kwargs={'certificate_uuid': cert.verify_uuid}) + '?evidence_visit=1'
def criteria(course_key):
"""
Constructs the 'criteria' URL from the course about page.
"""
about_path = reverse('about_course', kwargs={'course_id': str(course_key)})
return f'{site_prefix()}{about_path}'
def get_completion_badge(course_id, user):
"""
Given a course key and a user, find the user's enrollment mode
and get the Course Completion badge.
"""
from common.djangoapps.student.models import CourseEnrollment
badge_classes = CourseEnrollment.objects.filter(
user=user, course_id=course_id
).order_by('-is_active')
if not badge_classes:
return None
mode = badge_classes[0].mode
course = modulestore().get_course(course_id)
if not course.issue_badges:
return None
return BadgeClass.get_badge_class(
slug=course_slug(course_id, mode),
issuing_component='',
criteria=criteria(course_id),
description=badge_description(course, mode),
course_id=course_id,
mode=mode,
display_name=course.display_name,
image_file_handle=CourseCompleteImageConfiguration.image_for_mode(mode)
)
@requires_badges_enabled
def course_badge_check(user, course_key):
"""
Takes a GeneratedCertificate instance, and checks to see if a badge exists for this course, creating
it if not, should conditions be right.
"""
if not modulestore().get_course(course_key).issue_badges:
LOGGER.info("Course is not configured to issue badges.")
return
badge_class = get_completion_badge(course_key, user)
if not badge_class:
# We're not configured to make a badge for this course mode.
return
if BadgeAssertion.objects.filter(user=user, badge_class=badge_class):
LOGGER.info("Completion badge already exists for this user on this course.")
# Badge already exists. Skip.
return
evidence = evidence_url(user.id, course_key)
badge_class.award(user, evidence_url=evidence)
return

View File

@@ -1,82 +0,0 @@
"""
Events which have to do with a user doing something with more than one course, such
as enrolling in a certain number, completing a certain number, or completing a specific set of courses.
"""
from lms.djangoapps.badges.models import BadgeClass, CourseEventBadgesConfiguration
from lms.djangoapps.badges.utils import requires_badges_enabled
def award_badge(config, count, user):
"""
Given one of the configurations for enrollments or completions, award
the appropriate badge if one is configured.
config is a dictionary with integer keys and course keys as values.
count is the key to retrieve from this dictionary.
user is the user to award the badge to.
Example config:
{3: 'slug_for_badge_for_three_enrollments', 5: 'slug_for_badge_with_five_enrollments'}
"""
slug = config.get(count)
if not slug:
return
badge_class = BadgeClass.get_badge_class(
slug=slug, issuing_component='openedx__course', create=False,
)
if not badge_class:
return
if not badge_class.get_for_user(user):
badge_class.award(user)
def award_enrollment_badge(user):
"""
Awards badges based on the number of courses a user is enrolled in.
"""
config = CourseEventBadgesConfiguration.current().enrolled_settings
enrollments = user.courseenrollment_set.filter(is_active=True).count()
award_badge(config, enrollments, user)
@requires_badges_enabled
def completion_check(user):
"""
Awards badges based upon the number of courses a user has 'completed'.
Courses are never truly complete, but they can be closed.
For this reason we use checks on certificates to find out if a user has
completed courses. This badge will not work if certificate generation isn't
enabled and run.
"""
from lms.djangoapps.certificates.data import CertificateStatuses
config = CourseEventBadgesConfiguration.current().completed_settings
certificates = user.generatedcertificate_set.filter(status__in=CertificateStatuses.PASSED_STATUSES).count()
award_badge(config, certificates, user)
@requires_badges_enabled
def course_group_check(user, course_key):
"""
Awards a badge if a user has completed every course in a defined set.
"""
from lms.djangoapps.certificates.data import CertificateStatuses
config = CourseEventBadgesConfiguration.current().course_group_settings
awards = []
for slug, keys in config.items():
if course_key in keys:
certs = user.generatedcertificate_set.filter(
status__in=CertificateStatuses.PASSED_STATUSES,
course_id__in=keys,
)
if len(certs) == len(keys):
awards.append(slug)
for slug in awards:
badge_class = BadgeClass.get_badge_class(
slug=slug, issuing_component='openedx__course', create=False,
)
if badge_class and not badge_class.get_for_user(user):
badge_class.award(user)

View File

@@ -1,71 +0,0 @@
"""
Tests for the course completion helper functions.
"""
from datetime import datetime
from uuid import uuid4
from common.djangoapps.student.tests.factories import UserFactory
from lms.djangoapps.badges.events import course_complete
from lms.djangoapps.certificates.models import GeneratedCertificate
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
class CourseCompleteTestCase(ModuleStoreTestCase):
"""
Tests for the course completion helper functions.
"""
def setUp(self):
super().setUp()
# Need key to be deterministic to test slugs.
self.course = CourseFactory.create(
org='edX', course='course_test', run='test_run', display_name='Badged',
start=datetime(year=2015, month=5, day=19),
end=datetime(year=2015, month=5, day=20)
)
self.course_key = self.course.location.course_key
def test_slug(self):
"""
Verify slug generation is working as expected. If this test fails, the algorithm has changed, and it will cause
the handler to lose track of all badges it made in the past.
"""
assert course_complete.course_slug(self.course_key, 'honor') ==\
'course-v1edxcourse_testtest_run_honor_2055051'
assert course_complete.course_slug(self.course_key, 'verified') ==\
'course-v1edxcourse_testtest_run_verified_d550ad7'
def test_dated_description(self):
"""
Verify that a course with start/end dates contains a description with them.
"""
assert course_complete.badge_description(self.course, 'honor') ==\
'Completed the course "Badged" (honor, 2015-05-19 - 2015-05-20)'
def test_self_paced_description(self):
"""
Verify that a badge created for a course with no end date gets a different description.
"""
self.course.end = None
assert course_complete.badge_description(self.course, 'honor') == 'Completed the course "Badged" (honor)'
def test_evidence_url(self):
"""
Make sure the evidence URL points to the right place.
"""
user = UserFactory.create()
cert = GeneratedCertificate.eligible_certificates.create(
user=user,
course_id=self.course_key,
download_uuid=uuid4(),
grade="0.95",
key='the_key',
distinction=True,
status='downloadable',
mode='honor',
name=user.profile.name,
verify_uuid=uuid4().hex
)
assert f'https://edx.org/certificates/{cert.verify_uuid}?evidence_visit=1' ==\
course_complete.evidence_url(user.id, self.course_key)

View File

@@ -1,184 +0,0 @@
"""
Tests the course meta badging events
"""
from unittest.mock import patch
from ddt import data, ddt, unpack
from django.conf import settings
from django.test.utils import override_settings
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.tests.factories import UserFactory
from lms.djangoapps.badges.tests.factories import CourseEventBadgesConfigurationFactory, RandomBadgeClassFactory
from lms.djangoapps.certificates.data import CertificateStatuses
from lms.djangoapps.certificates.models import GeneratedCertificate
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
@ddt
@patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True})
@override_settings(BADGING_BACKEND='lms.djangoapps.badges.backends.tests.dummy_backend.DummyBackend')
class CourseEnrollmentBadgeTest(ModuleStoreTestCase):
"""
Tests the event which awards badges based on number of courses a user is enrolled in.
"""
def setUp(self):
super().setUp()
self.badge_classes = [
RandomBadgeClassFactory(
issuing_component='openedx__course'
),
RandomBadgeClassFactory(
issuing_component='openedx__course'
),
RandomBadgeClassFactory(
issuing_component='openedx__course'
),
]
nums = ['3', '5', '8']
entries = [','.join(pair) for pair in zip(nums, [badge.slug for badge in self.badge_classes])]
enrollment_config = '\r'.join(entries)
self.config = CourseEventBadgesConfigurationFactory(courses_enrolled=enrollment_config)
def test_no_match(self):
"""
Make sure a badge isn't created before a user's reached any checkpoint.
"""
user = UserFactory()
course = CourseFactory()
CourseEnrollment.enroll(user, course_key=course.location.course_key)
assert not user.badgeassertion_set.all()
@unpack
@data((1, 3), (2, 5), (3, 8))
def test_checkpoint_matches(self, checkpoint, required_badges):
"""
Make sure the proper badges are awarded at the right checkpoints.
"""
user = UserFactory()
courses = [CourseFactory() for _i in range(required_badges)]
for course in courses:
CourseEnrollment.enroll(user, course_key=course.location.course_key)
assertions = user.badgeassertion_set.all().order_by('id')
assert user.badgeassertion_set.all().count() == checkpoint
assert assertions[(checkpoint - 1)].badge_class == self.badge_classes[(checkpoint - 1)]
@ddt
@patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True})
@override_settings(BADGING_BACKEND='lms.djangoapps.badges.backends.tests.dummy_backend.DummyBackend')
class CourseCompletionBadgeTest(ModuleStoreTestCase):
"""
Tests the event which awards badges based on the number of courses completed.
"""
def setUp(self):
super().setUp()
self.badge_classes = [
RandomBadgeClassFactory(
issuing_component='openedx__course'
),
RandomBadgeClassFactory(
issuing_component='openedx__course'
),
RandomBadgeClassFactory(
issuing_component='openedx__course'
),
]
nums = ['2', '6', '9']
entries = [','.join(pair) for pair in zip(nums, [badge.slug for badge in self.badge_classes])]
completed_config = '\r'.join(entries)
self.config = CourseEventBadgesConfigurationFactory.create(courses_completed=completed_config)
self.config.clean_fields()
def test_no_match(self):
"""
Make sure a badge isn't created before a user's reached any checkpoint.
"""
user = UserFactory()
course = CourseFactory()
GeneratedCertificate(
user=user, course_id=course.location.course_key, status=CertificateStatuses.downloadable
).save()
assert not user.badgeassertion_set.all()
@unpack
@data((1, 2), (2, 6), (3, 9))
def test_checkpoint_matches(self, checkpoint, required_badges):
"""
Make sure the proper badges are awarded at the right checkpoints.
"""
user = UserFactory()
courses = [CourseFactory() for _i in range(required_badges)]
for course in courses:
GeneratedCertificate(
user=user, course_id=course.location.course_key, status=CertificateStatuses.downloadable
).save()
assertions = user.badgeassertion_set.all().order_by('id')
assert user.badgeassertion_set.all().count() == checkpoint
assert assertions[(checkpoint - 1)].badge_class == self.badge_classes[(checkpoint - 1)]
@patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True})
@override_settings(BADGING_BACKEND='lms.djangoapps.badges.backends.tests.dummy_backend.DummyBackend')
class CourseGroupBadgeTest(ModuleStoreTestCase):
"""
Tests the event which awards badges when a user completes a set of courses.
"""
def setUp(self):
super().setUp()
self.badge_classes = [
RandomBadgeClassFactory(
issuing_component='openedx__course'
),
RandomBadgeClassFactory(
issuing_component='openedx__course'
),
RandomBadgeClassFactory(
issuing_component='openedx__course'
),
]
self.courses = []
for _badge_class in self.badge_classes:
self.courses.append([CourseFactory().location.course_key for _i in range(3)]) # lint-amnesty, pylint: disable=no-member
lines = [badge_class.slug + ',' + ','.join([str(course_key) for course_key in keys])
for badge_class, keys in zip(self.badge_classes, self.courses)]
config = '\r'.join(lines)
self.config = CourseEventBadgesConfigurationFactory(course_groups=config)
self.config_map = dict(list(zip(self.badge_classes, self.courses)))
def test_no_match(self):
"""
Make sure a badge isn't created before a user's completed any course groups.
"""
user = UserFactory()
course = CourseFactory()
GeneratedCertificate(
user=user, course_id=course.location.course_key, status=CertificateStatuses.downloadable
).save()
assert not user.badgeassertion_set.all()
def test_group_matches(self):
"""
Make sure the proper badges are awarded when groups are completed.
"""
user = UserFactory()
items = list(self.config_map.items())
for badge_class, course_keys in items:
for i, key in enumerate(course_keys):
GeneratedCertificate(
user=user, course_id=key, status=CertificateStatuses.downloadable
).save()
# We don't award badges until all three are set.
if i + 1 == len(course_keys):
assert badge_class.get_for_user(user)
else:
assert not badge_class.get_for_user(user)
classes = [badge.badge_class.id for badge in user.badgeassertion_set.all()]
source_classes = [badge.id for badge in self.badge_classes]
assert classes == source_classes

View File

@@ -1,20 +0,0 @@
"""
Badges related signal handlers.
"""
from django.dispatch import receiver
from common.djangoapps.student.models import EnrollStatusChange
from common.djangoapps.student.signals import ENROLL_STATUS_CHANGE
from lms.djangoapps.badges.events.course_meta import award_enrollment_badge
from lms.djangoapps.badges.utils import badges_enabled
@receiver(ENROLL_STATUS_CHANGE)
def award_badge_on_enrollment(sender, event=None, user=None, **kwargs): # pylint: disable=unused-argument
"""
Awards enrollment badge to the given user on new enrollments.
"""
if badges_enabled and event == EnrollStatusChange.enroll:
award_enrollment_badge(user)

View File

@@ -3,24 +3,8 @@ Database models for the badges app
"""
from importlib import import_module
from config_models.models import ConfigurationModel
from django.conf import settings
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _
from jsonfield import JSONField
from lazy import lazy # lint-amnesty, pylint: disable=no-name-in-module
from model_utils.models import TimeStampedModel
from opaque_keys import InvalidKeyError
from opaque_keys.edx.django.models import CourseKeyField
from opaque_keys.edx.keys import CourseKey
from lms.djangoapps.badges.utils import deserialize_count_specs
from openedx.core.djangolib.markup import HTML, Text
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
def validate_badge_image(image):
@@ -39,298 +23,3 @@ def validate_lowercase(string):
"""
if not string.islower():
raise ValidationError(_("This value must be all lowercase."))
class CourseBadgesDisabledError(Exception):
"""
Exception raised when Course Badges aren't enabled, but an attempt to fetch one is made anyway.
"""
class BadgeClass(models.Model):
"""
Specifies a badge class to be registered with a backend.
.. no_pii:
"""
slug = models.SlugField(max_length=255, validators=[validate_lowercase])
badgr_server_slug = models.SlugField(max_length=255, default='', blank=True)
issuing_component = models.SlugField(max_length=50, default='', blank=True, validators=[validate_lowercase])
display_name = models.CharField(max_length=255)
course_id = CourseKeyField(max_length=255, blank=True, default=None)
description = models.TextField()
criteria = models.TextField()
# Mode a badge was awarded for. Included for legacy/migration purposes.
mode = models.CharField(max_length=100, default='', blank=True)
image = models.ImageField(upload_to='badge_classes', validators=[validate_badge_image])
def __str__(self): # lint-amnesty, pylint: disable=invalid-str-returned
return HTML("<Badge '{slug}' for '{issuing_component}'>").format(
slug=HTML(self.slug), issuing_component=HTML(self.issuing_component)
)
@classmethod
def get_badge_class(
cls, slug, issuing_component, display_name=None, description=None, criteria=None, image_file_handle=None,
mode='', course_id=None, create=True
):
# TODO method should be renamed to getorcreate instead
"""
Looks up a badge class by its slug, issuing component, and course_id and returns it should it exist.
If it does not exist, and create is True, creates it according to the arguments. Otherwise, returns None.
The expectation is that an XBlock or platform developer should not need to concern themselves with whether
or not a badge class has already been created, but should just feed all requirements to this function
and it will 'do the right thing'. It should be the exception, rather than the common case, that a badge class
would need to be looked up without also being created were it missing.
"""
slug = slug.lower()
issuing_component = issuing_component.lower()
if course_id and not modulestore().get_course(course_id).issue_badges:
raise CourseBadgesDisabledError("This course does not have badges enabled.")
if not course_id:
course_id = CourseKeyField.Empty
try:
return cls.objects.get(slug=slug, issuing_component=issuing_component, course_id=course_id)
except cls.DoesNotExist:
if not create:
return None
badge_class = cls(
slug=slug,
issuing_component=issuing_component,
display_name=display_name,
course_id=course_id,
mode=mode,
description=description,
criteria=criteria,
)
badge_class.image.save(image_file_handle.name, image_file_handle)
badge_class.full_clean()
badge_class.save()
return badge_class
@lazy
def backend(self):
"""
Loads the badging backend.
"""
module, klass = settings.BADGING_BACKEND.rsplit('.', 1)
module = import_module(module)
return getattr(module, klass)()
def get_for_user(self, user):
"""
Get the assertion for this badge class for this user, if it has been awarded.
"""
return self.badgeassertion_set.filter(user=user)
def award(self, user, evidence_url=None):
"""
Contacts the backend to have a badge assertion created for this badge class for this user.
"""
return self.backend.award(self, user, evidence_url=evidence_url) # lint-amnesty, pylint: disable=no-member
def save(self, **kwargs): # lint-amnesty, pylint: disable=arguments-differ
"""
Slugs must always be lowercase.
"""
self.slug = self.slug and self.slug.lower()
self.issuing_component = self.issuing_component and self.issuing_component.lower()
super().save(**kwargs)
class Meta:
app_label = "badges"
unique_together = (('slug', 'issuing_component', 'course_id'),)
verbose_name_plural = "Badge Classes"
class BadgeAssertion(TimeStampedModel):
"""
Tracks badges on our side of the badge baking transaction
.. no_pii:
"""
user = models.ForeignKey(User, on_delete=models.CASCADE)
badge_class = models.ForeignKey(BadgeClass, on_delete=models.CASCADE)
data = JSONField()
backend = models.CharField(max_length=50)
image_url = models.URLField()
assertion_url = models.URLField()
def __str__(self): # lint-amnesty, pylint: disable=invalid-str-returned
return HTML("<{username} Badge Assertion for {slug} for {issuing_component}").format(
username=HTML(self.user.username),
slug=HTML(self.badge_class.slug),
issuing_component=HTML(self.badge_class.issuing_component),
)
@classmethod
def assertions_for_user(cls, user, course_id=None):
"""
Get all assertions for a user, optionally constrained to a course.
"""
if course_id:
return cls.objects.filter(user=user, badge_class__course_id=course_id)
return cls.objects.filter(user=user)
class Meta:
app_label = "badges"
# Abstract model doesn't index this, so we have to.
BadgeAssertion._meta.get_field('created').db_index = True
class CourseCompleteImageConfiguration(models.Model):
"""
Contains the icon configuration for badges for a specific course mode.
.. no_pii:
"""
mode = models.CharField(
max_length=125,
help_text=_('The course mode for this badge image. For example, "verified" or "honor".'),
unique=True,
)
icon = models.ImageField(
# Actual max is 256KB, but need overhead for badge baking. This should be more than enough.
help_text=_(
"Badge images must be square PNG files. The file size should be under 250KB."
),
upload_to='course_complete_badges',
validators=[validate_badge_image]
)
default = models.BooleanField(
help_text=_(
"Set this value to True if you want this image to be the default image for any course modes "
"that do not have a specified badge image. You can have only one default image."
),
default=False,
)
def __str__(self): # lint-amnesty, pylint: disable=invalid-str-returned
return HTML("<CourseCompleteImageConfiguration for '{mode}'{default}>").format(
mode=HTML(self.mode),
default=HTML(" (default)") if self.default else HTML('')
)
def clean(self):
"""
Make sure there's not more than one default.
"""
if self.default and CourseCompleteImageConfiguration.objects.filter(default=True).exclude(id=self.id):
raise ValidationError(_("There can be only one default image."))
@classmethod
def image_for_mode(cls, mode):
"""
Get the image for a particular mode.
"""
try:
return cls.objects.get(mode=mode).icon
except cls.DoesNotExist:
# Fall back to default, if there is one.
return cls.objects.get(default=True).icon
class Meta:
app_label = "badges"
class CourseEventBadgesConfiguration(ConfigurationModel):
"""
Determines the settings for meta course awards-- such as completing a certain
number of courses or enrolling in a certain number of them.
.. no_pii:
"""
courses_completed = models.TextField(
blank=True, default='',
help_text=_(
"On each line, put the number of completed courses to award a badge for, a comma, and the slug of a "
"badge class you have created that has the issuing component 'openedx__course'. "
"For example: 3,enrolled_3_courses"
)
)
courses_enrolled = models.TextField(
blank=True, default='',
help_text=_(
"On each line, put the number of enrolled courses to award a badge for, a comma, and the slug of a "
"badge class you have created that has the issuing component 'openedx__course'. "
"For example: 3,enrolled_3_courses"
)
)
course_groups = models.TextField(
blank=True, default='',
help_text=_(
"Each line is a comma-separated list. The first item in each line is the slug of a badge class you "
"have created that has an issuing component of 'openedx__course'. The remaining items in each line are "
"the course keys the learner needs to complete to be awarded the badge. For example: "
"slug_for_compsci_courses_group_badge,course-v1:CompSci+Course+First,course-v1:CompsSci+Course+Second"
)
)
def __str__(self): # lint-amnesty, pylint: disable=invalid-str-returned
return HTML("<CourseEventBadgesConfiguration ({})>").format(
Text("Enabled") if self.enabled else Text("Disabled")
)
@property
def completed_settings(self):
"""
Parses the settings from the courses_completed field.
"""
return deserialize_count_specs(self.courses_completed)
@property
def enrolled_settings(self):
"""
Parses the settings from the courses_completed field.
"""
return deserialize_count_specs(self.courses_enrolled)
@property
def course_group_settings(self):
"""
Parses the course group settings. In example, the format is:
slug_for_compsci_courses_group_badge,course-v1:CompSci+Course+First,course-v1:CompsSci+Course+Second
"""
specs = self.course_groups.strip()
if not specs:
return {}
specs = [line.split(',', 1) for line in specs.splitlines()]
return {
slug.strip().lower(): [CourseKey.from_string(key.strip()) for key in keys.strip().split(',')]
for slug, keys in specs
}
def clean_fields(self, exclude=tuple()):
"""
Verify the settings are parseable.
"""
errors = {}
error_message = _("Please check the syntax of your entry.")
if 'courses_completed' not in exclude:
try:
self.completed_settings
except (ValueError, InvalidKeyError):
errors['courses_completed'] = [str(error_message)]
if 'courses_enrolled' not in exclude:
try:
self.enrolled_settings
except (ValueError, InvalidKeyError):
errors['courses_enrolled'] = [str(error_message)]
if 'course_groups' not in exclude:
store = modulestore()
try:
for key_list in self.course_group_settings.values():
for course_key in key_list:
if not store.get_course(course_key):
ValueError(f"The course {course_key} does not exist.")
except (ValueError, InvalidKeyError):
errors['course_groups'] = [str(error_message)]
if errors:
raise ValidationError(errors)
class Meta:
app_label = "badges"

View File

@@ -1,30 +0,0 @@
"""
Badging service for XBlocks
"""
from lms.djangoapps.badges.models import BadgeClass
class BadgingService:
"""
A class that provides functions for managing badges which XBlocks can use.
If course_enabled is True, course-level badges are permitted for this course.
If it is False, any badges that are awarded should be non-course specific.
"""
course_badges_enabled = False
def __init__(self, course_id=None, modulestore=None):
"""
Sets the 'course_badges_enabled' parameter.
"""
if not (course_id and modulestore):
return
course = modulestore.get_course(course_id)
if course:
self.course_badges_enabled = course.issue_badges
get_badge_class = BadgeClass.get_badge_class

View File

@@ -1,88 +0,0 @@
"""
Factories for Badge tests
"""
from random import random
import factory
from django.core.files.base import ContentFile
from factory.django import ImageField
from common.djangoapps.student.tests.factories import UserFactory
from lms.djangoapps.badges.models import ( # lint-amnesty, pylint: disable=line-too-long
BadgeAssertion,
BadgeClass,
CourseCompleteImageConfiguration,
CourseEventBadgesConfiguration
)
def generate_dummy_image(_unused):
"""
Used for image fields to create a sane default.
"""
return ContentFile(
ImageField()._make_data( # pylint: disable=protected-access
{'color': 'blue', 'width': 50, 'height': 50, 'format': 'PNG'}
), 'test.png'
)
class CourseCompleteImageConfigurationFactory(factory.django.DjangoModelFactory):
"""
Factory for BadgeImageConfigurations
"""
class Meta:
model = CourseCompleteImageConfiguration
mode = 'honor'
icon = factory.LazyAttribute(generate_dummy_image)
class BadgeClassFactory(factory.django.DjangoModelFactory):
"""
Factory for BadgeClass
"""
class Meta:
model = BadgeClass
slug = 'test_slug'
badgr_server_slug = 'test_badgr_server_slug'
issuing_component = 'test_component'
display_name = 'Test Badge'
description = "Yay! It's a test badge."
criteria = 'https://example.com/syllabus'
mode = 'honor'
image = factory.LazyAttribute(generate_dummy_image)
class RandomBadgeClassFactory(BadgeClassFactory):
"""
Same as BadgeClassFactory, but randomize the slug.
"""
slug = factory.lazy_attribute(lambda _: 'test_slug_' + str(random()).replace('.', '_'))
class BadgeAssertionFactory(factory.django.DjangoModelFactory):
"""
Factory for BadgeAssertions
"""
class Meta:
model = BadgeAssertion
user = factory.SubFactory(UserFactory)
badge_class = factory.SubFactory(RandomBadgeClassFactory)
data = {}
assertion_url = 'http://example.com/example.json'
image_url = 'http://example.com/image.png'
class CourseEventBadgesConfigurationFactory(factory.django.DjangoModelFactory):
"""
Factory for CourseEventsBadgesConfiguration
"""
class Meta:
model = CourseEventBadgesConfiguration
enabled = True

View File

@@ -1,311 +0,0 @@
"""
Tests for the Badges app models.
"""
import contextlib
from unittest.mock import Mock, patch
import pytest
from django.core.exceptions import ValidationError
from django.core.files.images import ImageFile
from django.core.files.storage import default_storage
from django.db.utils import IntegrityError
from django.test import TestCase
from django.test.utils import override_settings
from path import Path
from common.djangoapps.student.tests.factories import UserFactory
from lms.djangoapps.badges.models import (
BadgeAssertion,
BadgeClass,
CourseBadgesDisabledError,
CourseCompleteImageConfiguration,
validate_badge_image
)
from lms.djangoapps.badges.tests.factories import BadgeAssertionFactory, BadgeClassFactory, RandomBadgeClassFactory
from lms.djangoapps.certificates.tests.test_models import TEST_DATA_ROOT, TEST_DATA_DIR
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
@contextlib.contextmanager
def get_image(name):
"""
Get one of the test images from the test data directory.
"""
with open(f'{TEST_DATA_DIR}/badges/{name}.png', mode='rb') as fimage:
yield ImageFile(fimage)
@override_settings(MEDIA_ROOT=TEST_DATA_ROOT)
class BadgeImageConfigurationTest(TestCase):
"""
Test the validation features of BadgeImageConfiguration.
"""
def tearDown(self): # lint-amnesty, pylint: disable=super-method-not-called
tmp_path = Path(TEST_DATA_ROOT / 'course_complete_badges')
Path.rmtree_p(tmp_path)
def test_no_double_default(self):
"""
Verify that creating two configurations as default is not permitted.
"""
with get_image('good') as image_handle:
CourseCompleteImageConfiguration(mode='test', icon=ImageFile(image_handle), default=True).save()
with get_image('good') as image_handle:
pytest.raises(ValidationError, CourseCompleteImageConfiguration(mode='test2', icon=ImageFile(image_handle),
default=True).full_clean)
def test_runs_validator(self):
"""
Verify that the image validator is triggered when cleaning the model.
"""
with get_image('unbalanced') as image_handle:
pytest.raises(
ValidationError,
CourseCompleteImageConfiguration(mode='test2', icon=ImageFile(image_handle)).full_clean
)
class DummyBackend:
"""
Dummy badge backend, used for testing.
"""
award = Mock()
@override_settings(MEDIA_ROOT=TEST_DATA_ROOT)
class BadgeClassTest(ModuleStoreTestCase):
"""
Test BadgeClass functionality
"""
def setUp(self):
super().setUp()
self.addCleanup(self.cleanup_uploads)
def cleanup_uploads(self):
"""
Remove all files uploaded as badges.
"""
upload_to = BadgeClass._meta.get_field('image').upload_to
if default_storage.exists(upload_to):
(_, files) = default_storage.listdir(upload_to)
for uploaded_file in files:
default_storage.delete(upload_to + '/' + uploaded_file)
# Need full path to make sure class names line up.
@override_settings(BADGING_BACKEND='lms.djangoapps.badges.tests.test_models.DummyBackend')
def test_backend(self):
"""
Verify the BadgeClass fetches the backend properly.
"""
assert isinstance(BadgeClass().backend, DummyBackend)
def test_get_badge_class_preexisting(self):
"""
Verify fetching a badge first grabs existing badges.
"""
premade_badge_class = BadgeClassFactory.create()
# Ignore additional parameters. This class already exists.
with get_image('good') as image_handle:
badge_class = BadgeClass.get_badge_class(
slug='test_slug', issuing_component='test_component', description='Attempted override',
criteria='test', display_name='Testola', image_file_handle=image_handle
)
# These defaults are set on the factory.
assert badge_class.criteria == 'https://example.com/syllabus'
assert badge_class.display_name == 'Test Badge'
assert badge_class.description == "Yay! It's a test badge."
# File name won't always be the same.
assert badge_class.image.path == premade_badge_class.image.path
def test_unique_for_course(self):
"""
Verify that the course_id is used in fetching existing badges or creating new ones.
"""
course_key = CourseFactory.create().location.course_key
premade_badge_class = BadgeClassFactory.create(course_id=course_key)
with get_image('good') as image_handle:
badge_class = BadgeClass.get_badge_class(
slug='test_slug', issuing_component='test_component', description='Attempted override',
criteria='test', display_name='Testola', image_file_handle=image_handle
)
with get_image('good') as image_handle:
course_badge_class = BadgeClass.get_badge_class(
slug='test_slug', issuing_component='test_component', description='Attempted override',
criteria='test', display_name='Testola', image_file_handle=image_handle,
course_id=course_key,
)
assert badge_class.id != course_badge_class.id
assert course_badge_class.id == premade_badge_class.id
def test_get_badge_class_course_disabled(self):
"""
Verify attempting to fetch a badge class for a course which does not issue badges raises an
exception.
"""
course_key = CourseFactory.create(metadata={'issue_badges': False}).location.course_key
with pytest.raises(CourseBadgesDisabledError):
with get_image('good') as image_handle:
BadgeClass.get_badge_class(
slug='test_slug', issuing_component='test_component', description='Attempted override',
criteria='test', display_name='Testola', image_file_handle=image_handle,
course_id=course_key,
)
def test_get_badge_class_create(self):
"""
Verify fetching a badge creates it if it doesn't yet exist.
"""
with get_image('good') as image_handle:
badge_class = BadgeClass.get_badge_class(
slug='new_slug', issuing_component='new_component', description='This is a test',
criteria='https://example.com/test_criteria', display_name='Super Badge',
image_file_handle=image_handle
)
# This should have been saved before being passed back.
assert badge_class.id
assert badge_class.slug == 'new_slug'
assert badge_class.issuing_component == 'new_component'
assert badge_class.description == 'This is a test'
assert badge_class.criteria == 'https://example.com/test_criteria'
assert badge_class.display_name == 'Super Badge'
assert 'good' in badge_class.image.name.rsplit('/', 1)[(- 1)]
def test_get_badge_class_nocreate(self):
"""
Test returns None if the badge class does not exist.
"""
badge_class = BadgeClass.get_badge_class(
slug='new_slug', issuing_component='new_component', create=False
)
assert badge_class is None
# Run this twice to verify there wasn't a background creation of the badge.
badge_class = BadgeClass.get_badge_class(
slug='new_slug', issuing_component='new_component', description=None,
criteria=None, display_name=None,
image_file_handle=None, create=False
)
assert badge_class is None
def test_get_badge_class_image_validate(self):
"""
Verify handing a broken image to get_badge_class raises a validation error upon creation.
"""
# TODO Test should be updated, this doc doesn't makes sense, the object eventually gets created
with get_image('unbalanced') as image_handle:
self.assertRaises(
ValidationError,
BadgeClass.get_badge_class,
slug='new_slug', issuing_component='new_component', description='This is a test',
criteria='https://example.com/test_criteria', display_name='Super Badge',
image_file_handle=image_handle
)
def test_get_badge_class_data_validate(self):
"""
Verify handing incomplete data for required fields when making a badge class raises an Integrity error.
"""
with pytest.raises(IntegrityError), self.allow_transaction_exception():
with get_image('good') as image_handle:
BadgeClass.get_badge_class(
slug='new_slug', issuing_component='new_component', image_file_handle=image_handle
)
def test_get_for_user(self):
"""
Make sure we can get an assertion for a user if there is one.
"""
user = UserFactory.create()
badge_class = BadgeClassFactory.create()
assert not badge_class.get_for_user(user)
assertion = BadgeAssertionFactory.create(badge_class=badge_class, user=user)
assert list(badge_class.get_for_user(user)) == [assertion]
@override_settings(
BADGING_BACKEND='lms.djangoapps.badges.backends.badgr.BadgrBackend',
BADGR_USERNAME='example@example.com',
BADGR_PASSWORD='password',
BADGR_TOKENS_CACHE_KEY='badgr-test-cache-key')
@patch('lms.djangoapps.badges.backends.badgr.BadgrBackend.award')
def test_award(self, mock_award):
"""
Verify that the award command calls the award function on the backend with the right parameters.
"""
user = UserFactory.create()
badge_class = BadgeClassFactory.create()
badge_class.award(user, evidence_url='http://example.com/evidence')
assert mock_award.called
mock_award.assert_called_with(badge_class, user, evidence_url='http://example.com/evidence')
def test_runs_validator(self):
"""
Verify that the image validator is triggered when cleaning the model.
"""
with get_image('unbalanced') as image_handle:
pytest.raises(
ValidationError,
BadgeClass(
slug='test', issuing_component='test2', criteria='test3',
description='test4', image=ImageFile(image_handle)).full_clean
)
class BadgeAssertionTest(ModuleStoreTestCase):
"""
Tests for the BadgeAssertion model
"""
def test_assertions_for_user(self):
"""
Verify that grabbing all assertions for a user behaves as expected.
This function uses object IDs because for some reason Jenkins trips up
on its assertCountEqual check here despite the items being equal.
"""
user = UserFactory()
assertions = [BadgeAssertionFactory.create(user=user).id for _i in range(3)]
course = CourseFactory.create()
course_key = course.location.course_key
course_badges = [RandomBadgeClassFactory(course_id=course_key) for _i in range(3)]
course_assertions = [
BadgeAssertionFactory.create(user=user, badge_class=badge_class).id for badge_class in course_badges
]
assertions.extend(course_assertions)
assertions.sort()
assertions_for_user = [badge.id for badge in BadgeAssertion.assertions_for_user(user)]
assertions_for_user.sort()
assert assertions_for_user == assertions
course_scoped_assertions = [
badge.id for badge in BadgeAssertion.assertions_for_user(user, course_id=course_key)
]
course_scoped_assertions.sort()
assert course_scoped_assertions == course_assertions
class ValidBadgeImageTest(TestCase):
"""
Tests the badge image field validator.
"""
def test_good_image(self):
"""
Verify that saving a valid badge image is no problem.
"""
with get_image('good') as image_handle:
validate_badge_image(ImageFile(image_handle))
def test_unbalanced_image(self):
"""
Verify that setting an image with an uneven width and height raises an error.
"""
with get_image('unbalanced') as image_handle:
self.assertRaises(ValidationError, validate_badge_image, ImageFile(image_handle))
def test_large_image(self):
"""
Verify that setting an image that is too big raises an error.
"""
with get_image('large') as image_handle:
self.assertRaises(ValidationError, validate_badge_image, ImageFile(image_handle))

View File

@@ -1,48 +0,0 @@
"""
Utility functions used by the badging app.
"""
from django.conf import settings
def site_prefix():
"""
Get the prefix for the site URL-- protocol and server name.
"""
scheme = "https" if settings.HTTPS == "on" else "http"
return f'{scheme}://{settings.SITE_NAME}'
def requires_badges_enabled(function):
"""
Decorator that bails a function out early if badges aren't enabled.
"""
def wrapped(*args, **kwargs):
"""
Wrapped function which bails out early if bagdes aren't enabled.
"""
if not badges_enabled():
return
return function(*args, **kwargs)
return wrapped
def badges_enabled():
"""
returns a boolean indicating whether or not openbadges are enabled.
"""
return settings.FEATURES.get('ENABLE_OPENBADGES', False)
def deserialize_count_specs(text):
"""
Takes a string in the format of:
int,course_key
int,course_key
And returns a dictionary with the keys as the numbers and the values as the course keys.
"""
specs = text.splitlines()
specs = [line.split(',') for line in specs if line.strip()]
return {int(num): slug.strip().lower() for num, slug in specs}

View File

@@ -26,8 +26,6 @@ from simple_history.models import HistoricalRecords
from common.djangoapps.student import models_api as student_api
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.util.milestones_helpers import fulfill_course_milestone, is_prerequisite_courses_enabled
from lms.djangoapps.badges.events.course_complete import course_badge_check
from lms.djangoapps.badges.events.course_meta import completion_check, course_group_check
from lms.djangoapps.certificates.data import CertificateStatuses
from lms.djangoapps.instructor_task.models import InstructorTask
from openedx.core.djangoapps.signals.signals import COURSE_CERT_AWARDED, COURSE_CERT_CHANGED, COURSE_CERT_REVOKED
@@ -1245,31 +1243,6 @@ class CertificateTemplateAsset(TimeStampedModel):
app_label = "certificates"
@receiver(COURSE_CERT_AWARDED, sender=GeneratedCertificate)
# pylint: disable=unused-argument
def create_course_badge(sender, user, course_key, status, **kwargs):
"""
Standard signal hook to create course badges when a certificate has been generated.
"""
course_badge_check(user, course_key)
@receiver(COURSE_CERT_AWARDED, sender=GeneratedCertificate)
def create_completion_badge(sender, user, course_key, status, **kwargs): # pylint: disable=unused-argument
"""
Standard signal hook to create 'x courses completed' badges when a certificate has been generated.
"""
completion_check(user)
@receiver(COURSE_CERT_AWARDED, sender=GeneratedCertificate)
def create_course_group_badge(sender, user, course_key, status, **kwargs): # pylint: disable=unused-argument
"""
Standard signal hook to create badges when a user has completed a prespecified set of courses.
"""
course_group_check(user, course_key)
class CertificateGenerationCommandConfiguration(ConfigurationModel):
"""
Manages configuration for a run of the cert_generation management command.

View File

@@ -20,12 +20,6 @@ from common.djangoapps.student.roles import CourseStaffRole
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
from common.djangoapps.track.tests import EventTrackingTestCase
from common.djangoapps.util.date_utils import strftime_localized
from lms.djangoapps.badges.events.course_complete import get_completion_badge
from lms.djangoapps.badges.tests.factories import (
BadgeAssertionFactory,
BadgeClassFactory,
CourseCompleteImageConfigurationFactory
)
from lms.djangoapps.certificates.config import AUTO_CERTIFICATE_GENERATION
from lms.djangoapps.certificates.models import (
CertificateGenerationCourseSetting,
@@ -58,8 +52,6 @@ from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, p
FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy()
FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True
FEATURES_WITH_BADGES_ENABLED = FEATURES_WITH_CERTS_ENABLED.copy()
FEATURES_WITH_BADGES_ENABLED['ENABLE_OPENBADGES'] = True
FEATURES_WITH_CERTS_DISABLED = settings.FEATURES.copy()
FEATURES_WITH_CERTS_DISABLED['CERTIFICATES_HTML_VIEW'] = False
@@ -118,7 +110,6 @@ class CommonCertificatesTestCase(ModuleStoreTestCase):
)
CertificateHtmlViewConfigurationFactory.create()
LinkedInAddToProfileConfigurationFactory.create()
CourseCompleteImageConfigurationFactory.create()
def _add_course_certificates(self, count=1, signatory_count=0, is_active=True):
"""
@@ -533,32 +524,7 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase)
self.assertContains(response, f'<title>test_organization {self.course.number} Certificate |')
self.assertContains(response, 'logo_test1.png')
@ddt.data(True, False)
@patch('lms.djangoapps.certificates.views.webview.get_completion_badge')
def test_fetch_badge_info(self, issue_badges, mock_get_completion_badge):
"""
Test: Fetch badge class info if badges are enabled.
"""
if issue_badges:
features = FEATURES_WITH_BADGES_ENABLED
else:
features = FEATURES_WITH_CERTS_ENABLED
with override_settings(FEATURES=features):
badge_class = BadgeClassFactory(course_id=self.course_id, mode=self.cert.mode)
mock_get_completion_badge.return_value = badge_class
self._add_course_certificates(count=1, signatory_count=1, is_active=True)
test_url = get_certificate_url(user_id=self.user.id, course_id=self.cert.course_id,
uuid=self.cert.verify_uuid)
response = self.client.get(test_url)
assert response.status_code == 200
if issue_badges:
mock_get_completion_badge.assert_called()
else:
mock_get_completion_badge.assert_not_called()
@override_settings(FEATURES=FEATURES_WITH_BADGES_ENABLED)
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
@patch.dict("django.conf.settings.SOCIAL_SHARING_SETTINGS", {
"CERTIFICATE_TWITTER": True,
"CERTIFICATE_FACEBOOK": True,
@@ -589,10 +555,6 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase)
test_org = organizations_api.add_organization(organization_data=test_organization_data)
organizations_api.add_organization_course(organization_data=test_org, course_key=str(self.course.id))
self._add_course_certificates(count=1, signatory_count=1, is_active=True)
badge_class = get_completion_badge(course_id=self.course_id, user=self.user)
BadgeAssertionFactory.create(
user=self.user, badge_class=badge_class,
)
self.course.cert_html_view_overrides = {
"logo_src": "/static/certificates/images/course_override_logo.png"
}
@@ -629,8 +591,6 @@ class CertificatesViewsTests(CommonCertificatesTestCase, CacheIsolationTestCase)
partner_long_name=long_org_name,
),
)
# Test item from badge info
self.assertContains(response, "Add to Mozilla Backpack")
# Test item from site configuration
self.assertContains(response, "https://www.test-site.org/about-us")
# Test course overrides
@@ -1784,53 +1744,3 @@ class CertificateEventTests(CommonCertificatesTestCase, EventTrackingTestCase):
},
actual_event['data']
)
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
def test_evidence_event_sent(self):
self._add_course_certificates(count=1, signatory_count=2)
cert_url = get_certificate_url(
user_id=self.user.id,
course_id=self.course_id,
uuid=self.cert.verify_uuid
)
test_url = f'{cert_url}?evidence_visit=1'
self.recreate_tracker()
badge_class = get_completion_badge(self.course_id, self.user)
assertion = BadgeAssertionFactory.create(
user=self.user, badge_class=badge_class,
backend='DummyBackend',
image_url='https://www.example.com/image.png',
assertion_url='https://www.example.com/assertion.json',
data={
'issuer': 'https://www.example.com/issuer.json',
}
)
response = self.client.get(test_url)
# There are two events being emitted in this flow.
# One for page hit (due to the tracker in the middleware) and
# one due to the certificate being visited.
# We are interested in the second one.
actual_event = self.get_event(1)
assert response.status_code == 200
assert_event_matches(
{
'name': 'edx.badge.assertion.evidence_visited',
'data': {
'course_id': 'course-v1:testorg+run1+refundable_course',
'assertion_id': assertion.id,
'badge_generator': 'DummyBackend',
'badge_name': 'refundable course',
'issuing_component': '',
'badge_slug': 'course-v1testorgrun1refundable_course_honor_927f3ad',
'assertion_json_url': 'https://www.example.com/assertion.json',
'assertion_image_url': 'https://www.example.com/image.png',
'user_id': self.user.id,
'issuer': 'https://www.example.com/issuer.json',
'enrollment_mode': 'honor',
},
},
actual_event
)

View File

@@ -12,9 +12,8 @@ from milestones.tests.utils import MilestonesTestCaseMixin
from pytz import UTC
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
from common.djangoapps.student.tests.factories import UserFactory
from common.djangoapps.util.milestones_helpers import milestones_achieved_by_user, set_prerequisite_courses
from lms.djangoapps.badges.tests.factories import CourseCompleteImageConfigurationFactory
from lms.djangoapps.certificates.api import certificate_info_for_user, certificate_status_for_student
from lms.djangoapps.certificates.models import (
CertificateStatuses,
@@ -204,20 +203,3 @@ class CertificatesModelTest(ModuleStoreTestCase, MilestonesTestCaseMixin):
completed_milestones = milestones_achieved_by_user(student, str(pre_requisite_course.id))
assert len(completed_milestones) == 1
assert completed_milestones[0]['namespace'] == str(pre_requisite_course.id)
@patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True})
@patch('lms.djangoapps.badges.backends.badgr.BadgrBackend', spec=True)
def test_badge_callback(self, handler):
student = UserFactory()
course = CourseFactory.create(org='edx', number='998', display_name='Test Course', issue_badges=True)
CourseCompleteImageConfigurationFactory()
CourseEnrollmentFactory(user=student, course_id=course.location.course_key, mode='honor')
cert = GeneratedCertificateFactory.create(
user=student,
course_id=course.id,
status=CertificateStatuses.generating,
mode='verified'
)
cert.status = CertificateStatuses.downloadable
cert.save()
assert handler.return_value.award.called

View File

@@ -15,7 +15,6 @@ from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.template import RequestContext
from django.utils import translation
from django.utils.encoding import smart_str
from eventtracking import tracker
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from openedx_filters.learning.filters import CertificateRenderStarted
@@ -27,8 +26,6 @@ from common.djangoapps.edxmako.template import Template
from common.djangoapps.student.models import LinkedInAddToProfileConfiguration
from common.djangoapps.util.date_utils import strftime_localized
from common.djangoapps.util.views import handle_500
from lms.djangoapps.badges.events.course_complete import get_completion_badge
from lms.djangoapps.badges.utils import badges_enabled
from lms.djangoapps.certificates.api import (
certificates_viewable_for_course,
display_date_for_certificate,
@@ -396,42 +393,6 @@ def _track_certificate_events(request, course, user, user_certificate):
"""
Tracks web certificate view related events.
"""
# Badge Request Event Tracking Logic
course_key = course.location.course_key
if 'evidence_visit' in request.GET:
badge_class = get_completion_badge(course_key, user)
if not badge_class:
log.warning('Visit to evidence URL for badge, but badges not configured for course "%s"', course_key)
badges = []
else:
badges = badge_class.get_for_user(user)
if badges:
# There should only ever be one of these.
badge = badges[0]
tracker.emit(
'edx.badge.assertion.evidence_visited',
{
'badge_name': badge.badge_class.display_name,
'badge_slug': badge.badge_class.slug,
'badge_generator': badge.backend,
'issuing_component': badge.badge_class.issuing_component,
'user_id': user.id,
'course_id': str(course_key),
'enrollment_mode': badge.badge_class.mode,
'assertion_id': badge.id,
'assertion_image_url': badge.image_url,
'assertion_json_url': badge.assertion_url,
'issuer': badge.data.get('issuer'),
}
)
else:
log.warning(
"Could not find badge for %s on course %s.",
user.id,
course_key,
)
# track certificate evidence_visited event for analytics when certificate_user and accessing_user are different
if request.user and request.user.id != user.id:
emit_certificate_event('evidence_visited', user, str(course.id), event_data={
@@ -441,18 +402,6 @@ def _track_certificate_events(request, course, user, user_certificate):
})
def _update_badge_context(context, course, user):
"""
Updates context with badge info.
"""
badge = None
if badges_enabled() and course.issue_badges:
badges = get_completion_badge(course.location.course_key, user).get_for_user(user)
if badges:
badge = badges[0]
context['badge'] = badge
def _update_organization_context(context, course):
"""
Updates context with organization related info.
@@ -630,9 +579,6 @@ def render_html_view(request, course_id, certificate=None): # pylint: disable=t
# Append/Override the existing view context values with certificate specific values
_update_certificate_context(context, course, course_overview, user_certificate, platform_name)
# Append badge info
_update_badge_context(context, course, user)
# Add certificate header/footer data to current context
context.update(get_certificate_header_context(is_secure=request.is_secure()))
context.update(get_certificate_footer_context())

View File

@@ -40,8 +40,6 @@ from xblock.exceptions import NoSuchHandlerError, NoSuchViewError
from xblock.reference.plugins import FSService
from xblock.runtime import KvsFieldData
from lms.djangoapps.badges.service import BadgingService
from lms.djangoapps.badges.utils import badges_enabled
from lms.djangoapps.teams.services import TeamsService
from openedx.core.lib.xblock_services.call_to_action import CallToActionService
from xmodule.contentstore.django import contentstore
@@ -630,7 +628,6 @@ def prepare_runtime_for_user(
'partitions': PartitionService(course_id=course_id, cache=DEFAULT_REQUEST_CACHE.data),
'settings': SettingsService(),
'user_tags': UserTagsService(user=user, course_id=course_id),
'badging': BadgingService(course_id=course_id, modulestore=store) if badges_enabled() else None,
'teams': TeamsService(),
'teams_configuration': TeamsConfigurationService(),
'call_to_action': CallToActionService(),

View File

@@ -75,8 +75,6 @@ from common.djangoapps.xblock_django.constants import (
ATTR_KEY_USER_IS_STAFF,
ATTR_KEY_USER_ROLE,
)
from lms.djangoapps.badges.tests.factories import BadgeClassFactory
from lms.djangoapps.badges.tests.test_models import get_image
from lms.djangoapps.courseware import block_render as render
from lms.djangoapps.courseware.access_response import AccessResponse
from lms.djangoapps.courseware.courses import get_course_info_section, get_course_with_access
@@ -2377,46 +2375,6 @@ class LMSXBlockServiceBindingTest(LMSXBlockServiceMixin):
self.block.runtime.service(self.block, 'user_tags').get_tag('fake_scope', key)
@ddt.ddt
class TestBadgingService(LMSXBlockServiceMixin):
"""Test the badging service interface"""
@patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True})
def test_service_rendered(self):
self._prepare_runtime()
assert self.block.runtime.service(self.block, 'badging')
def test_no_service_rendered(self):
with pytest.raises(NoSuchServiceError):
self.block.runtime.service(self.block, 'badging')
@ddt.data(True, False)
@patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True})
def test_course_badges_toggle(self, toggle):
self.course = CourseFactory.create(metadata={'issue_badges': toggle})
self._prepare_runtime()
assert self.block.runtime.service(self.block, 'badging').course_badges_enabled is toggle
@patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True})
def test_get_badge_class(self):
self._prepare_runtime()
badge_service = self.block.runtime.service(self.block, 'badging')
premade_badge_class = BadgeClassFactory.create()
# Ignore additional parameters. This class already exists.
# We should get back the first class we created, rather than a new one.
with get_image('good') as image_handle:
badge_class = badge_service.get_badge_class(
slug='test_slug', issuing_component='test_component', description='Attempted override',
criteria='test', display_name='Testola', image_file_handle=image_handle
)
# These defaults are set on the factory.
assert badge_class.criteria == 'https://example.com/syllabus'
assert badge_class.display_name == 'Test Badge'
assert badge_class.description == "Yay! It's a test badge."
# File name won't always be the same.
assert badge_class.image.path == premade_badge_class.image.path
class TestI18nService(LMSXBlockServiceMixin):
""" Test XBlockI18nService """

View File

@@ -6,7 +6,6 @@ AWS_ACCESS_KEY_ID: ''
AWS_SECRET_ACCESS_KEY: ''
BUGS_EMAIL: bugs@example.com
BULK_EMAIL_DEFAULT_FROM_EMAIL: no-reply@example.com
BADGING_BACKEND: 'lms.djangoapps.badges.backends.tests.dummy_backend.DummyBackend'
BLOCK_STRUCTURES_SETTINGS:
# We have CELERY_ALWAYS_EAGER set to True, so there's no asynchronous
# code running and the celery routing is unimportant.
@@ -157,8 +156,6 @@ FEATURES:
ENABLE_DASHBOARD_SEARCH: True
# discussion home panel, which includes a subscription on/off setting for discussion digest emails.
ENABLE_DISCUSSION_HOME_PANEL: True
# Enable support for OpenBadges accomplishments
ENABLE_OPENBADGES: True
ENABLE_LTI_PROVIDER: True
# Enable milestones app
MILESTONES_APP: True

View File

@@ -696,19 +696,6 @@ FEATURES = {
# .. toggle_tickets: https://github.com/openedx/edx-platform/pull/9744
'ENABLE_SPECIAL_EXAMS': False,
# .. toggle_name: FEATURES['ENABLE_OPENBADGES']
# .. toggle_implementation: DjangoSetting
# .. toggle_default: False
# .. toggle_description: Enables support for the creation of OpenBadges as a method of awarding credentials.
# .. toggle_warning: The following settings (all of which are in the same file) should be set or reviewed prior to
# enabling this setting: BADGING_BACKEND, BADGR_API_TOKEN, BADGR_BASE_URL, BADGR_ISSUER_SLUG, BADGR_TIMEOUT.
# Full guide for setting up OpenBadges available here:
# https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/configuration/enable_badging.html pylint: disable=line-too-long,useless-suppression
# .. toggle_use_cases: open_edx
# .. toggle_creation_date: 2015-04-30
# .. toggle_tickets: https://openedx.atlassian.net/browse/SOL-1325
'ENABLE_OPENBADGES': False,
# .. toggle_name: FEATURES['ENABLE_LTI_PROVIDER']
# .. toggle_implementation: DjangoSetting
# .. toggle_default: False
@@ -3666,68 +3653,6 @@ REGISTRATION_EMAIL_PATTERNS_ALLOWED = None
CERT_NAME_SHORT = "Certificate"
CERT_NAME_LONG = "Certificate of Achievement"
#################### OpenBadges Settings #######################
# .. setting_name: BADGING_BACKEND
# .. setting_default: 'lms.djangoapps.badges.backends.badgr.BadgrBackend'
# .. setting_description: The backend service class (or callable) for creating OpenBadges. It must implement
# the interface provided by lms.djangoapps.badges.backends.base.BadgeBackend
# .. setting_warning: Review FEATURES['ENABLE_OPENBADGES'] for further context.
BADGING_BACKEND = 'lms.djangoapps.badges.backends.badgr.BadgrBackend'
# .. setting_name: BADGR_BASE_URL
# .. setting_default: 'http://localhost:8005'
# .. setting_description: The base URL for the Badgr server.
# .. setting_warning: DO NOT include a trailing slash. Review FEATURES['ENABLE_OPENBADGES'] for further context.
BADGR_BASE_URL = "http://localhost:8005"
# .. setting_name: BADGR_ISSUER_SLUG
# .. setting_default: 'example-issuer'
# .. setting_description: A string that is the slug for the Badgr issuer. The slug can be obtained from the URL of
# the Badgr Server page that displays the issuer. For example, in the URL
# http://exampleserver.com/issuer/test-issuer, the issuer slug is "test-issuer".
# .. setting_warning: Review FEATURES['ENABLE_OPENBADGES'] for further context.
BADGR_ISSUER_SLUG = "example-issuer"
# .. setting_name: BADGR_USERNAME
# .. setting_default: None
# .. setting_description: The username for Badgr. You should set up an issuer application with Badgr
# (https://badgr.org/app-developers/). The username and password will then be used to create or renew
# OAuth2 tokens.
# .. setting_warning: Review FEATURES['ENABLE_OPENBADGES'] for further context.
BADGR_USERNAME = None
# .. setting_name: BADGR_PASSWORD
# .. setting_default: None
# .. setting_description: The password for Badgr. You should set up an issuer application with Badgr
# (https://badgr.org/app-developers/). The username and password will then be used to create or renew
# OAuth2 tokens.
# .. setting_warning: Review FEATURES['ENABLE_OPENBADGES'] for further context.
BADGR_PASSWORD = None
# .. setting_name: BADGR_TOKENS_CACHE_KEY
# .. setting_default: None
# .. setting_description: The cache key for Badgr API tokens. Once created, the tokens will be stored in cache.
# Define the key here for setting and retrieveing the tokens.
# .. setting_warning: Review FEATURES['ENABLE_OPENBADGES'] for further context.
BADGR_TOKENS_CACHE_KEY = None
# .. setting_name: BADGR_TIMEOUT
# .. setting_default: 10
# .. setting_description: Number of seconds to wait on the badging server when contacting it before giving up.
# .. setting_warning: Review FEATURES['ENABLE_OPENBADGES'] for further context.
BADGR_TIMEOUT = 10
# .. toggle_name: BADGR_ENABLE_NOTIFICATIONS
# .. toggle_implementation: DjangoSetting
# .. toggle_default: False
# .. toggle_description: Optional setting for enabling email notifications. When set to "True",
# learners will be notified by email when they earn a badge.
# .. toggle_use_cases: open_edx
# .. toggle_creation_date: 2021-07-29
# .. toggle_warning: Review FEATURES['ENABLE_OPENBADGES'] for further context.
BADGR_ENABLE_NOTIFICATIONS = False
###################### Grade Downloads ######################
# These keys are used for all of our asynchronous downloadable files, including
# the ones that contain information other than grades.
@@ -4208,9 +4133,6 @@ ACCOUNT_VISIBILITY_CONFIGURATION["bulk_shareable_fields"] = (
"level_of_education",
'social_links',
'time_zone',
# Not an actual field, but used to signal whether badges should be public.
'accomplishments_shared',
]
)

View File

@@ -1,11 +0,0 @@
$(function() {
'use strict';
$('.action-share-mozillaopenbadges').click(function(event) {
$('.badges-overlay').fadeIn();
event.preventDefault();
});
$('.badges-modal .close').click(function() {
$('.badges-overlay').fadeOut();
});
});

View File

@@ -81,7 +81,7 @@ describe('Program Details Header View', () => {
expect(view.$('.org-logo').attr('alt'))
.toEqual(`${context.programData.authoring_organizations[0].name}'s logo`);
});
it('should render the subscription badge if subscription is active', () => {
expect(view.$('.meta-info .badge').html().trim()).toEqual('Subscribed');
});

View File

@@ -3,7 +3,6 @@ define(['underscore'], function(_) {
var USER_ACCOUNTS_API_URL = '/api/user/v0/accounts/student';
var USER_PREFERENCES_API_URL = '/api/user/v0/preferences/student';
var BADGES_API_URL = '/api/badges/v1/assertions/user/student/';
var IMAGE_UPLOAD_API_URL = '/api/profile_images/v0/staff/upload';
var IMAGE_REMOVE_API_URL = '/api/profile_images/v0/staff/remove';
var FIND_COURSES_URL = '/courses';
@@ -116,7 +115,6 @@ define(['underscore'], function(_) {
social_links: [{platform: 'facebook', social_link: 'https://www.facebook.com/edX'}],
language_proficiencies: [{code: '1'}],
profile_image: PROFILE_IMAGE,
accomplishments_shared: false
};
var DEFAULT_USER_PREFERENCES_DATA = {
'pref-lang': '2',
@@ -223,7 +221,6 @@ define(['underscore'], function(_) {
return {
USER_ACCOUNTS_API_URL: USER_ACCOUNTS_API_URL,
USER_PREFERENCES_API_URL: USER_PREFERENCES_API_URL,
BADGES_API_URL: BADGES_API_URL,
FIND_COURSES_URL: FIND_COURSES_URL,
IMAGE_UPLOAD_API_URL: IMAGE_UPLOAD_API_URL,
IMAGE_REMOVE_API_URL: IMAGE_REMOVE_API_URL,

View File

@@ -25,7 +25,6 @@
language_proficiencies: [],
requires_parental_consent: true,
profile_image: null,
accomplishments_shared: false,
default_public_account_fields: [],
extended_profile: [],
secondary_email: ''

View File

@@ -788,13 +788,9 @@
'js/spec/views/message_banner_spec.js',
'js/spec/views/notification_spec.js',
'learner_profile/js/spec/learner_profile_factory_spec.js',
'learner_profile/js/spec/views/badge_list_container_spec.js',
'learner_profile/js/spec/views/badge_list_view_spec.js',
'learner_profile/js/spec/views/badge_view_spec.js',
'learner_profile/js/spec/views/learner_profile_fields_spec.js',
'learner_profile/js/spec/views/learner_profile_view_spec.js',
'learner_profile/js/spec/views/section_two_tab_spec.js',
'learner_profile/js/spec/views/share_modal_view_spec.js',
'support/js/spec/collections/enrollment_spec.js',
'support/js/spec/models/enrollment_spec.js',
'support/js/spec/views/certificates_spec.js',

View File

@@ -227,11 +227,6 @@ if settings.FEATURES.get('ENABLE_MOBILE_REST_API'):
re_path(r'^api/mobile/(?P<api_version>v(2|1|0.5))/', include('lms.djangoapps.mobile_api.urls')),
]
if settings.FEATURES.get('ENABLE_OPENBADGES'):
urlpatterns += [
path('api/badges/v1/', include(('lms.djangoapps.badges.api.urls', 'badges'), namespace='badges_api')),
]
urlpatterns += [
path('openassessment/fileupload/', include('openassessment.fileupload.urls')),
]

View File

@@ -11,7 +11,7 @@ COURSE_GRADE_CHANGED = Signal()
# Signal that fires when a user is awarded a certificate in a course (in the certificates django app)
# TODO: runtime coupling between apps will be reduced if this event is changed to carry a username
# rather than a User object; however, this will require changes to the milestones and badges APIs
# rather than a User object; however, this will require changes to the milestones
# Same providing_args=["user", "course_key", "mode", "status"] for next 3 signals.
COURSE_CERT_CHANGED = Signal()
COURSE_CERT_AWARDED = Signal()

View File

@@ -21,7 +21,6 @@ from common.djangoapps.student.models import (
UserPasswordToggleHistory,
UserProfile
)
from lms.djangoapps.badges.utils import badges_enabled
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.user_api import errors
from openedx.core.djangoapps.user_api.accounts.utils import is_secondary_email_feature_enabled
@@ -136,7 +135,6 @@ class UserReadOnlySerializer(serializers.Serializer): # lint-amnesty, pylint: d
except ObjectDoesNotExist:
activation_key = None
accomplishments_shared = badges_enabled()
data = {
"username": user.username,
"url": self.context.get('request').build_absolute_uri(
@@ -164,7 +162,6 @@ class UserReadOnlySerializer(serializers.Serializer): # lint-amnesty, pylint: d
"level_of_education": None,
"mailing_address": None,
"requires_parental_consent": None,
"accomplishments_shared": accomplishments_shared,
"account_privacy": self.configuration.get('default_visibility'),
"social_links": None,
"extended_profile_fields": None,

View File

@@ -625,7 +625,6 @@ class AccountSettingsOnCreationTest(CreateAccountMixin, TestCase):
'requires_parental_consent': True,
'language_proficiencies': [],
'account_privacy': PRIVATE_VISIBILITY,
'accomplishments_shared': False,
'extended_profile': [],
'secondary_email': None,
'secondary_email_enabled': None,

View File

@@ -359,7 +359,7 @@ class TestAccountsAPI(FilteredQueryCountMixin, CacheIsolationTestCase, UserAPITe
ENABLED_CACHES = ['default']
TOTAL_QUERY_COUNT = 24
FULL_RESPONSE_FIELD_COUNT = 30
FULL_RESPONSE_FIELD_COUNT = 29
def setUp(self):
super().setUp()
@@ -379,12 +379,12 @@ class TestAccountsAPI(FilteredQueryCountMixin, CacheIsolationTestCase, UserAPITe
legacy_profile.save()
return year_of_birth
def _verify_full_shareable_account_response(self, response, account_privacy=None, badges_enabled=False):
def _verify_full_shareable_account_response(self, response, account_privacy=None):
"""
Verify that the shareable fields from the account are returned
"""
data = response.data
assert 12 == len(data)
assert 11 == len(data)
# public fields (3)
assert account_privacy == data['account_privacy']
@@ -399,7 +399,6 @@ class TestAccountsAPI(FilteredQueryCountMixin, CacheIsolationTestCase, UserAPITe
assert 'm' == data['level_of_education']
assert data['social_links'] is not None
assert data['time_zone'] is None
assert badges_enabled == data['accomplishments_shared']
def _verify_private_account_response(self, response, requires_parental_consent=False):
"""
@@ -436,7 +435,6 @@ class TestAccountsAPI(FilteredQueryCountMixin, CacheIsolationTestCase, UserAPITe
assert 'm' == data['level_of_education']
assert data['social_links'] is not None
assert UserPreference.get_value(self.user, 'time_zone') == data['time_zone']
assert data['accomplishments_shared'] is not None
assert ((self.user.first_name + ' ') + self.user.last_name) == data['name']
# additional admin fields (13)
@@ -669,7 +667,6 @@ class TestAccountsAPI(FilteredQueryCountMixin, CacheIsolationTestCase, UserAPITe
response = self.send_get(self.different_client)
self._verify_private_account_response(response)
@mock.patch.dict(settings.FEATURES, {'ENABLE_OPENBADGES': True})
@ddt.data(
("client", "user", PRIVATE_VISIBILITY),
("different_client", "different_user", PRIVATE_VISIBILITY),
@@ -691,7 +688,7 @@ class TestAccountsAPI(FilteredQueryCountMixin, CacheIsolationTestCase, UserAPITe
if preference_visibility == PRIVATE_VISIBILITY:
self._verify_private_account_response(response)
else:
self._verify_full_shareable_account_response(response, ALL_USERS_VISIBILITY, badges_enabled=True)
self._verify_full_shareable_account_response(response, ALL_USERS_VISIBILITY)
client = self.login_client(api_client, requesting_username)
@@ -812,8 +809,6 @@ class TestAccountsAPI(FilteredQueryCountMixin, CacheIsolationTestCase, UserAPITe
assert [] == data['language_proficiencies']
assert PRIVATE_VISIBILITY == data['account_privacy']
assert data['time_zone'] is None
# Badges aren't on by default, so should not be present.
assert data['accomplishments_shared'] is False
self.client.login(username=self.user.username, password=TEST_PASSWORD)
verify_get_own_information(self._get_num_queries(22))

View File

@@ -244,9 +244,6 @@ class AccountViewSet(ViewSet):
If "custom", the user has selectively chosen a subset of shareable
fields to make visible to others via the User Preferences API.
* accomplishments_shared: Signals whether badges are enabled on the
platform and should be fetched.
* phone_number: The phone number for the user. String of numbers with
an optional `+` sign at the start.

View File

@@ -14,14 +14,12 @@
'js/views/fields',
'learner_profile/js/views/learner_profile_fields',
'learner_profile/js/views/learner_profile_view',
'learner_profile/js/models/badges_model',
'learner_profile/js/views/badge_list_container',
'js/student_account/views/account_settings_fields',
'js/views/message_banner',
'string_utils'
], function(gettext, $, _, Backbone, Logger, StringUtils, PagingCollection, AccountSettingsModel,
AccountPreferencesModel, FieldsView, LearnerProfileFieldsView, LearnerProfileView, BadgeModel,
BadgeListContainer, AccountSettingsFieldViews, MessageBannerView) {
AccountPreferencesModel, FieldsView, LearnerProfileFieldsView, LearnerProfileView,
AccountSettingsFieldViews, MessageBannerView) {
return function(options) {
var $learnerProfileElement = $('.wrapper-profile');
@@ -55,9 +53,6 @@
nameFieldView,
sectionOneFieldViews,
sectionTwoFieldViews,
BadgeCollection,
badgeCollection,
badgeListContainer,
learnerProfileView,
getProfileVisibility,
showLearnerProfileView;
@@ -172,26 +167,6 @@
})
];
BadgeCollection = PagingCollection.extend({
queryParams: {
currentPage: 'current_page'
}
});
badgeCollection = new BadgeCollection();
badgeCollection.url = options.badges_api_url;
badgeListContainer = new BadgeListContainer({
attributes: {class: 'badge-set-display'},
collection: badgeCollection,
find_courses_url: options.find_courses_url,
ownProfile: options.own_profile,
badgeMeta: {
badges_logo: options.badges_logo,
backpack_ui_img: options.backpack_ui_img,
badges_icon: options.badges_icon
}
});
learnerProfileView = new LearnerProfileView({
el: $learnerProfileElement,
ownProfile: options.own_profile,
@@ -204,7 +179,6 @@
nameFieldView: nameFieldView,
sectionOneFieldViews: sectionOneFieldViews,
sectionTwoFieldViews: sectionTwoFieldViews,
badgeListContainer: badgeListContainer,
platformName: options.platform_name
});
@@ -239,7 +213,6 @@
accountSettingsModel: accountSettingsModel,
accountPreferencesModel: accountPreferencesModel,
learnerProfileView: learnerProfileView,
badgeListContainer: badgeListContainer
};
};
});

View File

@@ -1,8 +0,0 @@
(function(define) {
'use strict';
define(['backbone'], function(Backbone) {
var BadgesModel = Backbone.Model.extend({});
return BadgesModel;
});
}).call(this, define || RequireJS.define);

View File

@@ -31,7 +31,6 @@ define(
return new LearnerProfilePage({
accounts_api_url: Helpers.USER_ACCOUNTS_API_URL,
preferences_api_url: Helpers.USER_PREFERENCES_API_URL,
badges_api_url: Helpers.BADGES_API_URL,
own_profile: ownProfile,
account_settings_page_url: Helpers.USER_ACCOUNTS_API_URL,
country_options: Helpers.FIELD_OPTIONS,
@@ -68,148 +67,6 @@ define(
LearnerProfileHelpers.expectLimitedProfileSectionsAndFieldsToBeRendered(learnerProfileView);
});
it("doesn't show the mode toggle if badges are disabled", function() {
var requests = AjaxHelpers.requests(this),
context = createProfilePage(true, {accomplishments_shared: false}),
tabbedView = context.learnerProfileView.tabbedView,
learnerProfileView = context.learnerProfileView;
LearnerProfileHelpers.expectTabbedViewToBeUndefined(requests, tabbedView);
LearnerProfileHelpers.expectBadgesHidden(learnerProfileView);
});
it("doesn't show the mode toggle if badges fail to fetch", function() {
var requests = AjaxHelpers.requests(this),
context = createProfilePage(true, {accomplishments_shared: false}),
tabbedView = context.learnerProfileView.tabbedView,
learnerProfileView = context.learnerProfileView;
LearnerProfileHelpers.expectTabbedViewToBeUndefined(requests, tabbedView);
LearnerProfileHelpers.expectBadgesHidden(learnerProfileView);
});
it('renders the mode toggle if there are badges', function() {
var requests = AjaxHelpers.requests(this),
context = createProfilePage(true, {accomplishments_shared: true}),
tabbedView = context.learnerProfileView.tabbedView;
AjaxHelpers.expectRequest(requests, 'POST', '/event');
AjaxHelpers.respondWithError(requests, 404);
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.firstPageBadges);
LearnerProfileHelpers.expectTabbedViewToBeShown(tabbedView);
});
it('renders the mode toggle if badges enabled but none exist', function() {
var requests = AjaxHelpers.requests(this),
context = createProfilePage(true, {accomplishments_shared: true}),
tabbedView = context.learnerProfileView.tabbedView;
AjaxHelpers.expectRequest(requests, 'POST', '/event');
AjaxHelpers.respondWithError(requests, 404);
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.emptyBadges);
LearnerProfileHelpers.expectTabbedViewToBeShown(tabbedView);
});
it('displays the badges when the accomplishments toggle is selected', function() {
var requests = AjaxHelpers.requests(this),
context = createProfilePage(true, {accomplishments_shared: true}),
learnerProfileView = context.learnerProfileView,
tabbedView = learnerProfileView.tabbedView;
AjaxHelpers.expectRequest(requests, 'POST', '/event');
AjaxHelpers.respondWithError(requests, 404);
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.secondPageBadges);
LearnerProfileHelpers.expectBadgesHidden(learnerProfileView);
tabbedView.$el.find('[data-url="accomplishments"]').click();
LearnerProfileHelpers.expectBadgesDisplayed(learnerProfileView, 10, false);
tabbedView.$el.find('[data-url="about_me"]').click();
LearnerProfileHelpers.expectBadgesHidden(learnerProfileView);
});
it('displays a placeholder on the last page of badges', function() {
var requests = AjaxHelpers.requests(this),
context = createProfilePage(true, {accomplishments_shared: true}),
learnerProfileView = context.learnerProfileView,
tabbedView = learnerProfileView.tabbedView;
AjaxHelpers.expectRequest(requests, 'POST', '/event');
AjaxHelpers.respondWithError(requests, 404);
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.thirdPageBadges);
LearnerProfileHelpers.expectBadgesHidden(learnerProfileView);
tabbedView.$el.find('[data-url="accomplishments"]').click();
LearnerProfileHelpers.expectBadgesDisplayed(learnerProfileView, 10, true);
tabbedView.$el.find('[data-url="about_me"]').click();
LearnerProfileHelpers.expectBadgesHidden(learnerProfileView);
});
it('displays a placeholder when the accomplishments toggle is selected and no badges exist', function() {
var requests = AjaxHelpers.requests(this),
context = createProfilePage(true, {accomplishments_shared: true}),
learnerProfileView = context.learnerProfileView,
tabbedView = learnerProfileView.tabbedView;
AjaxHelpers.expectRequest(requests, 'POST', '/event');
AjaxHelpers.respondWithError(requests, 404);
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.emptyBadges);
LearnerProfileHelpers.expectBadgesHidden(learnerProfileView);
tabbedView.$el.find('[data-url="accomplishments"]').click();
LearnerProfileHelpers.expectBadgesDisplayed(learnerProfileView, 0, true);
tabbedView.$el.find('[data-url="about_me"]').click();
LearnerProfileHelpers.expectBadgesHidden(learnerProfileView);
});
it('shows a paginated list of badges', function() {
var requests = AjaxHelpers.requests(this),
context = createProfilePage(true, {accomplishments_shared: true}),
learnerProfileView = context.learnerProfileView,
tabbedView = learnerProfileView.tabbedView;
AjaxHelpers.expectRequest(requests, 'POST', '/event');
AjaxHelpers.respondWithError(requests, 404);
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.firstPageBadges);
tabbedView.$el.find('[data-url="accomplishments"]').click();
LearnerProfileHelpers.expectBadgesDisplayed(learnerProfileView, 10, false);
LearnerProfileHelpers.expectPage(learnerProfileView, LearnerProfileHelpers.firstPageBadges);
});
it('allows forward and backward navigation of badges', function() {
var requests = AjaxHelpers.requests(this),
context = createProfilePage(true, {accomplishments_shared: true}),
learnerProfileView = context.learnerProfileView,
tabbedView = learnerProfileView.tabbedView,
badgeListContainer = context.badgeListContainer;
AjaxHelpers.expectRequest(requests, 'POST', '/event');
AjaxHelpers.respondWithError(requests, 404);
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.firstPageBadges);
tabbedView.$el.find('[data-url="accomplishments"]').click();
badgeListContainer.$el.find('.next-page-link').click();
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.secondPageBadges);
LearnerProfileHelpers.expectPage(learnerProfileView, LearnerProfileHelpers.secondPageBadges);
badgeListContainer.$el.find('.next-page-link').click();
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.thirdPageBadges);
LearnerProfileHelpers.expectBadgesDisplayed(learnerProfileView, 10, true);
LearnerProfileHelpers.expectPage(learnerProfileView, LearnerProfileHelpers.thirdPageBadges);
badgeListContainer.$el.find('.previous-page-link').click();
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.secondPageBadges);
LearnerProfileHelpers.expectPage(learnerProfileView, LearnerProfileHelpers.secondPageBadges);
LearnerProfileHelpers.expectBadgesDisplayed(learnerProfileView, 10, false);
badgeListContainer.$el.find('.previous-page-link').click();
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.firstPageBadges);
LearnerProfileHelpers.expectPage(learnerProfileView, LearnerProfileHelpers.firstPageBadges);
});
it('renders the limited profile for under 13 users', function() {
var context = createProfilePage(
true,

View File

@@ -1,99 +0,0 @@
define([
'backbone',
'jquery',
'underscore',
'URI',
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
'edx-ui-toolkit/js/pagination/paging-collection',
'learner_profile/js/spec_helpers/helpers',
'learner_profile/js/views/badge_list_container'
],
function(Backbone, $, _, URI, AjaxHelpers, PagingCollection, LearnerProfileHelpers, BadgeListContainer) {
'use strict';
describe('edx.user.BadgeListContainer', function() {
var view;
var createView = function(requests, pageNum, badgeListObject) {
var BadgeCollection = PagingCollection.extend({
queryParams: {
currentPage: 'current_page'
}
});
var badgeCollection = new BadgeCollection();
var models = [];
var badgeListContainer;
var request;
var path;
badgeCollection.url = '/api/badges/v1/assertions/user/staff/';
_.each(_.range(badgeListObject.count), function(idx) {
models.push(LearnerProfileHelpers.makeBadge(idx));
});
badgeListObject.results = models; // eslint-disable-line no-param-reassign
badgeCollection.setPage(pageNum);
request = AjaxHelpers.currentRequest(requests);
path = new URI(request.url).path();
expect(path).toBe('/api/badges/v1/assertions/user/staff/');
AjaxHelpers.respondWithJson(requests, badgeListObject);
badgeListContainer = new BadgeListContainer({
collection: badgeCollection
});
badgeListContainer.render();
return badgeListContainer;
};
afterEach(function() {
view.$el.remove();
});
it('displays all badges', function() {
var requests = AjaxHelpers.requests(this),
badges;
view = createView(requests, 1, {
count: 30,
previous: '/arbitrary/url',
num_pages: 3,
next: null,
start: 20,
current_page: 1,
results: []
});
badges = view.$el.find('div.badge-display');
expect(badges.length).toBe(30);
});
it('displays placeholder on last page', function() {
var requests = AjaxHelpers.requests(this),
placeholder;
view = createView(requests, 3, {
count: 30,
previous: '/arbitrary/url',
num_pages: 3,
next: null,
start: 20,
current_page: 3,
results: []
});
placeholder = view.$el.find('span.accomplishment-placeholder');
expect(placeholder.length).toBe(1);
});
it('does not display placeholder on first page', function() {
var requests = AjaxHelpers.requests(this),
placeholder;
view = createView(requests, 1, {
count: 30,
previous: '/arbitrary/url',
num_pages: 3,
next: null,
start: 0,
current_page: 1,
results: []
});
placeholder = view.$el.find('span.accomplishment-placeholder');
expect(placeholder.length).toBe(0);
});
});
}
);

View File

@@ -1,81 +0,0 @@
define([
'backbone',
'jquery',
'underscore',
'edx-ui-toolkit/js/pagination/paging-collection',
'learner_profile/js/spec_helpers/helpers',
'learner_profile/js/views/badge_list_view'
],
function(Backbone, $, _, PagingCollection, LearnerProfileHelpers, BadgeListView) {
'use strict';
describe('edx.user.BadgeListView', function() {
var view;
var createView = function(badges, pages, page, hasNextPage) {
var badgeCollection = new PagingCollection();
var models = [];
var badgeList;
badgeCollection.url = '/api/badges/v1/assertions/user/staff/';
_.each(badges, function(element) {
models.push(new Backbone.Model(element));
});
badgeCollection.models = models;
badgeCollection.length = badges.length;
badgeCollection.currentPage = page;
badgeCollection.totalPages = pages;
badgeCollection.hasNextPage = function() {
return hasNextPage;
};
badgeList = new BadgeListView({
collection: badgeCollection
});
return badgeList;
};
afterEach(function() {
view.$el.remove();
});
it('there is a single row if there is only one badge', function() {
var rows;
view = createView([LearnerProfileHelpers.makeBadge(1)], 1, 1, false);
view.render();
rows = view.$el.find('div.row');
expect(rows.length).toBe(1);
});
it('accomplishments placeholder is visible on a last page', function() {
var placeholder;
view = createView([LearnerProfileHelpers.makeBadge(1)], 2, 2, false);
view.render();
placeholder = view.$el.find('span.accomplishment-placeholder');
expect(placeholder.length).toBe(1);
});
it('accomplishments placeholder to be not visible on a first page', function() {
var placeholder;
view = createView([LearnerProfileHelpers.makeBadge(1)], 1, 2, true);
view.render();
placeholder = view.$el.find('span.accomplishment-placeholder');
expect(placeholder.length).toBe(0);
});
it('badges are in two columns (checked by counting rows for a known number of badges)', function() {
var badges = [];
var placeholder;
var rows;
_.each(_.range(4), function(item) {
badges.push(LearnerProfileHelpers.makeBadge(item));
});
view = createView(badges, 1, 2, true);
view.render();
placeholder = view.$el.find('span.accomplishment-placeholder');
expect(placeholder.length).toBe(0);
rows = view.$el.find('div.row');
expect(rows.length).toBe(2);
});
});
}
);

View File

@@ -1,114 +0,0 @@
define([
'backbone', 'jquery', 'underscore',
'learner_profile/js/spec_helpers/helpers',
'learner_profile/js/views/badge_view'
],
function(Backbone, $, _, LearnerProfileHelpers, BadgeView) {
'use strict';
describe('edx.user.BadgeView', function() {
var view,
badge,
testBadgeNameIsDisplayed,
testBadgeIconIsDisplayed;
var createView = function(ownProfile) {
var options,
testView;
badge = LearnerProfileHelpers.makeBadge(1);
options = {
model: new Backbone.Model(badge),
ownProfile: ownProfile,
badgeMeta: {}
};
testView = new BadgeView(options);
testView.render();
$('body').append(testView.$el);
testView.$el.show();
expect(testView.$el.is(':visible')).toBe(true);
return testView;
};
afterEach(function() {
view.$el.remove();
$('.badges-modal').remove();
});
it('profile of other has no share button', function() {
view = createView(false);
expect(view.context.ownProfile).toBeFalsy();
expect(view.$el.find('button.share-button').length).toBe(0);
});
it('own profile has share button', function() {
view = createView(true);
expect(view.context.ownProfile).toBeTruthy();
expect(view.$el.find('button.share-button').length).toBe(1);
});
it('click on share button calls createModal function', function() {
var shareButton;
view = createView(true);
spyOn(view, 'createModal');
view.delegateEvents();
expect(view.context.ownProfile).toBeTruthy();
shareButton = view.$el.find('button.share-button');
expect(shareButton.length).toBe(1);
expect(view.createModal).not.toHaveBeenCalled();
shareButton.click();
expect(view.createModal).toHaveBeenCalled();
});
it('click on share button calls shows the dialog', function(done) {
var shareButton,
$modalElement;
view = createView(true);
expect(view.context.ownProfile).toBeTruthy();
shareButton = view.$el.find('button.share-button');
expect(shareButton.length).toBe(1);
$modalElement = $('.badges-modal');
expect($modalElement.length).toBe(0);
expect($modalElement.is(':visible')).toBeFalsy();
shareButton.click();
// Note: this element should have appeared in the dom during: shareButton.click();
$modalElement = $('.badges-modal');
jasmine.waitUntil(function() {
return $modalElement.is(':visible');
}).always(done);
});
testBadgeNameIsDisplayed = function(ownProfile) {
var badgeDiv;
view = createView(ownProfile);
badgeDiv = view.$el.find('.badge-name');
expect(badgeDiv.length).toBeTruthy();
expect(badgeDiv.is(':visible')).toBe(true);
expect(_.count(badgeDiv.html(), badge.badge_class.display_name)).toBeTruthy();
};
it('test badge name is displayed for own profile', function() {
testBadgeNameIsDisplayed(true);
});
it('test badge name is displayed for other profile', function() {
testBadgeNameIsDisplayed(false);
});
testBadgeIconIsDisplayed = function(ownProfile) {
var badgeImg;
view = createView(ownProfile);
badgeImg = view.$el.find('img.badge');
expect(badgeImg.length).toBe(1);
expect(badgeImg.attr('src')).toEqual(badge.image_url);
};
it('test badge icon is displayed for own profile', function() {
testBadgeIconIsDisplayed(true);
});
it('test badge icon is displayed for other profile', function() {
testBadgeIconIsDisplayed(false);
});
});
}
);

View File

@@ -15,13 +15,12 @@ define(
'js/student_account/models/user_preferences_model',
'learner_profile/js/views/learner_profile_fields',
'learner_profile/js/views/learner_profile_view',
'learner_profile/js/views/badge_list_container',
'js/student_account/views/account_settings_fields',
'js/views/message_banner'
],
function(gettext, Backbone, $, _, PagingCollection, AjaxHelpers, TemplateHelpers, Helpers, LearnerProfileHelpers,
FieldViews, UserAccountModel, AccountPreferencesModel, LearnerProfileFields, LearnerProfileView,
BadgeListContainer, AccountSettingsFieldViews, MessageBannerView) {
AccountSettingsFieldViews, MessageBannerView) {
'use strict';
describe('edx.user.LearnerProfileView', function() {
@@ -132,15 +131,6 @@ define(
})
];
var badgeCollection = new PagingCollection();
badgeCollection.url = Helpers.BADGES_API_URL;
var badgeListContainer = new BadgeListContainer({
attributes: {class: 'badge-set-display'},
collection: badgeCollection,
find_courses_url: Helpers.FIND_COURSES_URL
});
return new LearnerProfileView(
{
el: $('.wrapper-profile'),
@@ -154,7 +144,6 @@ define(
profileImageFieldView: profileImageFieldView,
sectionOneFieldViews: sectionOneFieldViews,
sectionTwoFieldViews: sectionTwoFieldViews,
badgeListContainer: badgeListContainer
});
};
@@ -225,16 +214,5 @@ define(
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
LearnerProfileHelpers.expectLimitedProfileSectionsAndFieldsToBeRendered(learnerProfileView, true);
});
it("renders an error if the badges can't be fetched", function() {
var learnerProfileView = createLearnerProfileView(false, 'all_users', true);
learnerProfileView.options.accountSettingsModel.set({accomplishments_shared: true});
var requests = AjaxHelpers.requests(this);
learnerProfileView.render();
LearnerProfileHelpers.breakBadgeLoading(learnerProfileView, requests);
LearnerProfileHelpers.expectBadgeLoadingErrorIsRendered(learnerProfileView);
});
});
});

View File

@@ -1,63 +0,0 @@
define(
[
'backbone', 'jquery', 'underscore', 'moment',
'js/spec/student_account/helpers',
'learner_profile/js/spec_helpers/helpers',
'learner_profile/js/views/share_modal_view',
'jquery.simulate'
],
function(Backbone, $, _, Moment, Helpers, LearnerProfileHelpers, ShareModalView) {
'use strict';
describe('edx.user.ShareModalView', function() {
var keys = $.simulate.keyCode;
var view;
var createModalView = function() {
var badge = LearnerProfileHelpers.makeBadge(1);
var context = _.extend(badge, {
created: new Moment(badge.created),
ownProfile: true,
badgeMeta: {}
});
return new ShareModalView({
model: new Backbone.Model(context),
shareButton: $('<button/>')
});
};
beforeEach(function() {
view = createModalView();
// Attach view to document, otherwise click won't work
view.render();
$('body').append(view.$el);
view.$el.show();
expect(view.$el.is(':visible')).toBe(true);
});
afterEach(function() {
view.$el.remove();
});
it('modal view closes on escape', function() {
spyOn(view, 'close');
view.delegateEvents();
expect(view.close).not.toHaveBeenCalled();
$(view.$el).simulate('keydown', {keyCode: keys.ESCAPE});
expect(view.close).toHaveBeenCalled();
});
it('modal view closes click on close', function() {
var $closeButton;
spyOn(view, 'close');
view.delegateEvents();
$closeButton = view.$el.find('button.close');
expect($closeButton.length).toBe(1);
expect(view.close).not.toHaveBeenCalled();
$closeButton.trigger('click');
expect(view.close).toHaveBeenCalled();
});
});
}
);

View File

@@ -123,137 +123,11 @@ define(['underscore', 'URI', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'
expect(tabbedViewView.$el.find('.page-content-nav').is(':visible')).toBe(true);
};
var expectBadgesDisplayed = function(learnerProfileView, length, lastPage) {
var $badgeListingView = $('#tabpanel-accomplishments'),
updatedLength = length,
placeholder;
expect($('#tabpanel-about_me').hasClass('is-hidden')).toBe(true);
expect($badgeListingView.hasClass('is-hidden')).toBe(false);
if (lastPage) {
updatedLength += 1;
placeholder = $badgeListingView.find('.find-course');
expect(placeholder.length).toBe(1);
expect(placeholder.attr('href')).toBe('/courses/');
}
expect($badgeListingView.find('.badge-display').length).toBe(updatedLength);
};
var expectBadgesHidden = function() {
var $accomplishmentsTab = $('#tabpanel-accomplishments');
if ($accomplishmentsTab.length) {
// Nonexistence counts as hidden.
expect($('#tabpanel-accomplishments').hasClass('is-hidden')).toBe(true);
}
expect($('#tabpanel-about_me').hasClass('is-hidden')).toBe(false);
};
var expectPage = function(learnerProfileView, pageData) {
var $badgeListContainer = $('#tabpanel-accomplishments');
var index = $badgeListContainer.find('span.search-count').text().trim();
expect(index).toBe('Showing ' + (pageData.start + 1) + '-' + (pageData.start + pageData.results.length)
+ ' out of ' + pageData.count + ' total');
expect($badgeListContainer.find('.current-page').text()).toBe('' + pageData.current_page);
_.each(pageData.results, function(badge) {
expect($('.badge-display:contains(' + badge.badge_class.display_name + ')').length).toBe(1);
});
};
var expectBadgeLoadingErrorIsRendered = function() {
var errorMessage = $('.badge-set-display').text();
expect(errorMessage).toBe(
'Your request could not be completed. Reload the page and try again. If the issue persists, click the '
+ 'Help tab to report the problem.'
);
};
var breakBadgeLoading = function(learnerProfileView, requests) {
var request = AjaxHelpers.currentRequest(requests);
var path = new URI(request.url).path();
expect(path).toBe('/api/badges/v1/assertions/user/student/');
AjaxHelpers.respondWithError(requests, 500);
};
var firstPageBadges = {
count: 30,
previous: null,
next: '/arbitrary/url',
num_pages: 3,
start: 0,
current_page: 1,
results: []
};
var secondPageBadges = {
count: 30,
previous: '/arbitrary/url',
next: '/arbitrary/url',
num_pages: 3,
start: 10,
current_page: 2,
results: []
};
var thirdPageBadges = {
count: 30,
previous: '/arbitrary/url',
num_pages: 3,
next: null,
start: 20,
current_page: 3,
results: []
};
var emptyBadges = {
count: 0,
previous: null,
num_pages: 1,
results: []
};
function makeBadge(num) {
return {
badge_class: {
slug: 'test_slug_' + num,
issuing_component: 'test_component',
display_name: 'Test Badge ' + num,
course_id: null,
description: "Yay! It's a test badge.",
criteria: 'https://example.com/syllabus',
image_url: 'http://localhost:8000/media/badge_classes/test_lMB9bRw.png'
},
image_url: 'http://example.com/image.png',
assertion_url: 'http://example.com/example.json',
created_at: '2015-12-03T16:25:57.676113Z'
};
}
_.each(_.range(0, 10), function(i) {
firstPageBadges.results.push(makeBadge(i));
});
_.each(_.range(10, 20), function(i) {
secondPageBadges.results.push(makeBadge(i));
});
_.each(_.range(20, 30), function(i) {
thirdPageBadges.results.push(makeBadge(i));
});
return {
expectLimitedProfileSectionsAndFieldsToBeRendered: expectLimitedProfileSectionsAndFieldsToBeRendered,
expectProfileSectionsAndFieldsToBeRendered: expectProfileSectionsAndFieldsToBeRendered,
expectProfileSectionsNotToBeRendered: expectProfileSectionsNotToBeRendered,
expectTabbedViewToBeUndefined: expectTabbedViewToBeUndefined,
expectTabbedViewToBeShown: expectTabbedViewToBeShown,
expectBadgesDisplayed: expectBadgesDisplayed,
expectBadgesHidden: expectBadgesHidden,
expectBadgeLoadingErrorIsRendered: expectBadgeLoadingErrorIsRendered,
breakBadgeLoading: breakBadgeLoading,
firstPageBadges: firstPageBadges,
secondPageBadges: secondPageBadges,
thirdPageBadges: thirdPageBadges,
emptyBadges: emptyBadges,
expectPage: expectPage,
makeBadge: makeBadge
expectTabbedViewToBeShown: expectTabbedViewToBeShown
};
});

View File

@@ -1,35 +0,0 @@
/* eslint-disable no-underscore-dangle */
(function(define) {
'use strict';
define(
[
'gettext', 'jquery', 'underscore', 'common/js/components/views/paginated_view',
'learner_profile/js/views/badge_view', 'learner_profile/js/views/badge_list_view',
'text!learner_profile/templates/badge_list.underscore'
],
function(gettext, $, _, PaginatedView, BadgeView, BadgeListView, BadgeListTemplate) {
var BadgeListContainer = PaginatedView.extend({
type: 'badge',
itemViewClass: BadgeView,
listViewClass: BadgeListView,
viewTemplate: BadgeListTemplate,
isZeroIndexed: true,
paginationLabel: gettext('Accomplishments Pagination'),
initialize: function(options) {
BadgeListContainer.__super__.initialize.call(this, options);
this.listView.find_courses_url = options.find_courses_url;
this.listView.badgeMeta = options.badgeMeta;
this.listView.ownProfile = options.ownProfile;
}
});
return BadgeListContainer;
});
}).call(this, define || RequireJS.define);

View File

@@ -1,65 +0,0 @@
(function(define) {
'use strict';
define([
'gettext',
'jquery',
'underscore',
'edx-ui-toolkit/js/utils/html-utils',
'common/js/components/views/list',
'learner_profile/js/views/badge_view',
'text!learner_profile/templates/badge_placeholder.underscore'
],
function(gettext, $, _, HtmlUtils, ListView, BadgeView, badgePlaceholder) {
var BadgeListView = ListView.extend({
tagName: 'div',
template: HtmlUtils.template(badgePlaceholder),
renderCollection: function() {
var self = this,
$row;
this.$el.empty();
// Split into two columns.
this.collection.each(function(badge, index) {
var $item;
if (index % 2 === 0) {
$row = $('<div class="row">');
this.$el.append($row);
}
$item = new BadgeView({
model: badge,
badgeMeta: this.badgeMeta,
ownProfile: this.ownProfile
}).render().el;
if ($row) {
$row.append($item);
}
this.itemViews.push($item);
}, this);
// Placeholder must always be at the end, and may need a new row.
if (!this.collection.hasNextPage()) {
// find_courses_url set by BadgeListContainer during initialization.
if (this.collection.length % 2 === 0) {
$row = $('<div class="row">');
this.$el.append($row);
}
if ($row) {
HtmlUtils.append(
$row,
this.template({find_courses_url: self.find_courses_url})
);
}
}
return this;
}
});
return BadgeListView;
});
}).call(this, define || RequireJS.define);

View File

@@ -1,47 +0,0 @@
(function(define) {
'use strict';
define(
[
'gettext', 'jquery', 'underscore', 'backbone', 'moment',
'text!learner_profile/templates/badge.underscore',
'learner_profile/js/views/share_modal_view',
'edx-ui-toolkit/js/utils/html-utils'
],
function(gettext, $, _, Backbone, Moment, badgeTemplate, ShareModalView, HtmlUtils) {
var BadgeView = Backbone.View.extend({
initialize: function(options) {
this.options = _.extend({}, options);
this.context = _.extend(this.options.model.toJSON(), {
created: new Moment(this.options.model.toJSON().created),
ownProfile: options.ownProfile,
badgeMeta: options.badgeMeta
});
},
attributes: {
class: 'badge-display'
},
template: _.template(badgeTemplate),
events: {
'click .share-button': 'createModal'
},
createModal: function() {
var modal = new ShareModalView({
model: new Backbone.Model(this.context),
shareButton: this.shareButton
});
modal.$el.hide();
modal.render();
$('body').append(modal.$el);
modal.$el.fadeIn('short', 'swing', _.bind(modal.ready, modal));
},
render: function() {
this.$el.html(HtmlUtils.HTML(this.template(this.context)).toString());
this.shareButton = this.$el.find('.share-button');
return this;
}
});
return BadgeView;
});
}).call(this, define || RequireJS.define);

View File

@@ -59,30 +59,12 @@
$('.wrapper-profile-section-container-one').removeClass('is-hidden');
$('.wrapper-profile-section-container-two').removeClass('is-hidden');
// Only show accomplishments if this is a full profile
if (this.showFullProfile()) {
$('.learner-achievements').removeClass('is-hidden');
} else {
$('.learner-achievements').addClass('is-hidden');
}
if (this.showFullProfile() && (this.options.accountSettingsModel.get('accomplishments_shared'))) {
tabs = [
{view: this.sectionTwoView, title: gettext('About Me'), url: 'about_me'},
{
view: this.options.badgeListContainer,
title: gettext('Accomplishments'),
url: 'accomplishments'
}
{view: this.sectionTwoView, title: gettext('About Me'), url: 'about_me'}
];
// Build the accomplishments Tab and fill with data
this.options.badgeListContainer.collection.fetch().done(function() {
self.options.badgeListContainer.render();
}).error(function() {
self.options.badgeListContainer.renderError();
});
this.tabbedView = new TabbedView({
tabs: tabs,
router: this.router,

View File

@@ -1,56 +0,0 @@
(function(define) {
'use strict';
define(
[
'gettext', 'jquery', 'underscore', 'backbone', 'moment',
'text!learner_profile/templates/share_modal.underscore',
'edx-ui-toolkit/js/utils/html-utils'
],
function(gettext, $, _, Backbone, Moment, badgeModalTemplate, HtmlUtils) {
var ShareModalView = Backbone.View.extend({
attributes: {
class: 'badges-overlay'
},
template: _.template(badgeModalTemplate),
events: {
'click .badges-modal': function(event) { event.stopPropagation(); },
'click .badges-modal .close': 'close',
'click .badges-overlay': 'close',
keydown: 'keyAction',
'focus .focusguard-start': 'focusGuardStart',
'focus .focusguard-end': 'focusGuardEnd'
},
initialize: function(options) {
this.options = _.extend({}, options);
},
focusGuardStart: function() {
// Should only be selected directly if shift-tabbing from the start, so grab last item.
this.$el.find('a').last().focus();
},
focusGuardEnd: function() {
this.$el.find('.badges-modal').focus();
},
close: function() {
this.$el.fadeOut('short', 'swing', _.bind(this.remove, this));
this.options.shareButton.focus();
},
keyAction: function(event) {
if (event.keyCode === $.ui.keyCode.ESCAPE) {
this.close();
}
},
ready: function() {
// Focusing on the modal background directly doesn't work, probably due
// to its positioning.
this.$el.find('.badges-modal').focus();
},
render: function() {
this.$el.html(HtmlUtils.HTML(this.template(this.model.toJSON())).toString());
return this;
}
});
return ShareModalView;
});
}).call(this, define || RequireJS.define);

View File

@@ -1,30 +0,0 @@
<div class="badge-image-container">
<img class="badge" src="<%- image_url %>" alt=""/>
</div>
<div class="badge-details">
<div class="badge-name"><%- badge_class.display_name %></div>
<p class="badge-description"><%- badge_class.description %></p>
<% if (ownProfile) { %>
<button class="share-button">
<div class="share-icon-container">
<img class="icon icon-mozillaopenbadges" src="<%- badgeMeta.badges_icon %>" alt="<%-
interpolate(
// Translators: display_name is the name of an OpenBadges award.
gettext('Share your "%(display_name)s" award'),
{'display_name': badge_class.display_name},
true
)%>">
</div>
<div class="share-prefix" aria-hidden="true"><%- gettext("Share") %></div>
</div>
<% } %>
<div class="badge-date-stamp">
<%-
interpolate(
// Translators: Date stamp for earned badges. Example: Earned December 3, 2015.
gettext('Earned %(created)s.'),
{created: created.format('LL')},
true
)
%></div>
</div>

View File

@@ -1,4 +0,0 @@
<div class="sr-is-focusable sr-<%- type %>-view" tabindex="-1"></div>
<div class="<%- type %>-paging-header"></div>
<div class="<%- type %>-list cards-list"></div>
<div class="<%- type %>-paging-footer"></div>

View File

@@ -1,10 +0,0 @@
<div class="badge-display badge-placeholder">
<div class="badge-image-container">
<span class="accomplishment-placeholder" aria-hidden="true">
</div>
<div class="badge-details">
<div class="badge-name"><%- gettext("What's Your Next Accomplishment?") %></div>
<p class="badge-description"><%- gettext('Start working toward your next learning goal.') %></p>
<a class="find-course" href="<%- find_courses_url %>"><span class="find-button-container"><%- gettext('Find a course') %></span></a>
</div>
</div>

View File

@@ -1,41 +0,0 @@
<div class="focusguard focusguard-start" tabindex="0"></div>
<div class="badges-modal" tabindex="0">
<button class="close"><span class="fa fa-close" aria-hidden="true"></span><span class="sr"><%- gettext("Close") %></span></button>
<h1 class="modal-header"><%- gettext("Share on Mozilla Backpack") %></h1>
<p class="explanation"><%- gettext("To share your certificate on Mozilla Backpack, you must first have a Backpack account. Complete the following steps to add your certificate to Backpack.") %>
</p>
<hr class="modal-hr"/>
<img class="backpack-logo" src="<%- badgeMeta.badges_logo %>" alt="">
<ol class="badges-steps">
<li class="step">
<%= edx.HtmlUtils.interpolateHtml(
gettext("Create a {link_start}Mozilla Backpack{link_end} account, or log in to your existing account"),
{
link_start: edx.HtmlUtils.HTML('<a href="https://backpack.openbadges.org/" rel="noopener" target="_blank">'),
link_end: edx.HtmlUtils.HTML('</a>')
}
)
%>
</li>
<li class="step">
<%= edx.HtmlUtils.interpolateHtml(
gettext("{download_link_start}Download this image (right-click or option-click, save as){link_end} and then {upload_link_start}upload{link_end} it to your backpack."),
{
download_link_start: edx.HtmlUtils.joinHtml(
edx.HtmlUtils.HTML('<a class="badge-link" href="'),
image_url,
edx.HtmlUtils.HTML('" rel="noopener" target="_blank">'),
),
link_end: edx.HtmlUtils.HTML('</a>'),
upload_link_start: edx.HtmlUtils.HTML('<a href="https://backpack.openbadges.org/backpack/add" rel="noopener" target="_blank">')
}
)
%>
</li>
</ol>
<div class="image-container">
<img class="badges-backpack-example" src="<%- badgeMeta.backpack_ui_img %>" alt="">
</div>
</div>
<div class="focusguard focusguard-end" tabindex="0"></div>

View File

@@ -11,7 +11,6 @@ from django.urls import reverse
from django.views.decorators.http import require_http_methods
from django_countries import countries
from lms.djangoapps.badges.utils import badges_enabled
from common.djangoapps.edxmako.shortcuts import marketing_link
from openedx.core.djangoapps.credentials.utils import get_credentials_records_url
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
@@ -105,8 +104,6 @@ def learner_profile_context(request, profile_username, user_is_staff):
'country_options': list(countries),
'find_courses_url': marketing_link('COURSES'),
'language_options': settings.ALL_LANGUAGES,
'badges_logo': staticfiles_storage.url('certificates/images/backpack-logo.png'),
'badges_icon': staticfiles_storage.url('certificates/images/ico-mozillaopenbadges.png'),
'backpack_ui_img': staticfiles_storage.url('certificates/images/backpack-ui.png'),
'platform_name': configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME),
'social_platforms': settings.SOCIAL_PLATFORMS,
@@ -128,7 +125,4 @@ def learner_profile_context(request, profile_username, user_is_staff):
)
context['achievements_fragment'] = achievements_fragment
if badges_enabled():
context['data']['badges_api_url'] = reverse("badges_api:user_assertions", kwargs={'username': profile_username})
return context

View File

@@ -99,9 +99,6 @@ module.exports = {
'../openedx/features/course_search/static/course_search/js/views/dashboard_search_results_view.js'
),
path.resolve(__dirname, '../openedx/features/course_search/static/course_search/js/views/search_results_view.js'),
path.resolve(__dirname, '../openedx/features/learner_profile/static/learner_profile/js/views/badge_list_container.js'),
path.resolve(__dirname, '../openedx/features/learner_profile/static/learner_profile/js/views/badge_list_view.js'),
path.resolve(__dirname, '../openedx/features/learner_profile/static/learner_profile/js/views/badge_view.js'),
path.resolve(
__dirname,
'../openedx/features/learner_profile/static/learner_profile/js/views/learner_profile_fields.js'

View File

@@ -619,14 +619,6 @@ class CourseFields: # lint-amnesty, pylint: disable=missing-class-docstring
# Ensure that courses imported from XML keep their image
default="images_course_image.jpg"
)
issue_badges = Boolean(
display_name=_("Issue Open Badges"),
help=_(
"Issue Open Badges badges for this course. Badges are generated when certificates are created."
),
scope=Scope.settings,
default=True
)
## Course level Certificate Name overrides.
cert_name_short = String(
help=_(