From 0b3e4d73b6fb6f41ae87cf2b77bca12052ee1ac8 Mon Sep 17 00:00:00 2001 From: thezaeemaanwar Date: Tue, 5 Sep 2023 16:21:12 +0500 Subject: [PATCH] feat!: Removed Badges App fix: restored badges handlers feat: remove FE code for badges fix: resolved failing tests fix: removed test case for badges app fix: unused import error fix: Response Field Count fix: shareable account response length fix: resolved PR comments fix: revert settings override feat!: Removed Badges App fix: restored badges handlers feat: remove FE code for badges fix: resolved failing tests fix: removed test case for badges app fix: unused import error fix: Response Field Count fix: shareable account response length fix: revert subscription badge --- .github/workflows/unit-test-shards.json | 1 - .../models/settings/course_metadata.py | 5 - cms/envs/bok_choy.py | 3 - cms/envs/common.py | 3 - common/djangoapps/util/tests/test_db.py | 2 + docs/lms-openapi.yaml | 177 ---------- docs/references/docstrings/lms_index.rst | 1 - lms/djangoapps/badges/admin.py | 20 -- lms/djangoapps/badges/api/__init__.py | 0 lms/djangoapps/badges/api/serializers.py | 30 -- lms/djangoapps/badges/api/tests.py | 218 ------------ lms/djangoapps/badges/api/urls.py | 14 - lms/djangoapps/badges/api/views.py | 139 -------- lms/djangoapps/badges/apps.py | 6 - lms/djangoapps/badges/backends/__init__.py | 0 lms/djangoapps/badges/backends/badgr.py | 324 ------------------ lms/djangoapps/badges/backends/base.py | 18 - .../badges/backends/tests/__init__.py | 0 .../badges/backends/tests/dummy_backend.py | 15 - .../backends/tests/test_badgr_backend.py | 301 ---------------- .../badges/events/course_complete.py | 70 +--- lms/djangoapps/badges/events/course_meta.py | 82 ----- .../badges/events/tests/__init__.py | 0 .../events/tests/test_course_complete.py | 71 ---- .../badges/events/tests/test_course_meta.py | 184 ---------- lms/djangoapps/badges/handlers.py | 20 -- lms/djangoapps/badges/models.py | 311 ----------------- lms/djangoapps/badges/service.py | 30 -- lms/djangoapps/badges/tests/__init__.py | 0 lms/djangoapps/badges/tests/factories.py | 88 ----- lms/djangoapps/badges/tests/test_models.py | 311 ----------------- lms/djangoapps/badges/utils.py | 48 --- lms/djangoapps/certificates/models.py | 27 -- .../certificates/tests/test_webview_views.py | 92 +---- lms/djangoapps/certificates/tests/tests.py | 20 +- lms/djangoapps/certificates/views/webview.py | 54 --- lms/djangoapps/courseware/block_render.py | 3 - .../courseware/tests/test_block_render.py | 42 --- lms/envs/bok_choy.yml | 3 - lms/envs/common.py | 78 ----- lms/static/js/certificates/certificates.js | 11 - .../spec/program_details_header_spec.js | 2 +- lms/static/js/spec/student_account/helpers.js | 3 - .../models/user_account_model.js | 1 - lms/static/lms/js/spec/main.js | 4 - lms/urls.py | 5 - openedx/core/djangoapps/signals/signals.py | 2 +- .../user_api/accounts/serializers.py | 3 - .../user_api/accounts/tests/test_api.py | 1 - .../user_api/accounts/tests/test_views.py | 13 +- .../djangoapps/user_api/accounts/views.py | 3 - .../js/learner_profile_factory.js | 31 +- .../learner_profile/js/models/badges_model.js | 8 - .../js/spec/learner_profile_factory_spec.js | 143 -------- .../spec/views/badge_list_container_spec.js | 99 ------ .../js/spec/views/badge_list_view_spec.js | 81 ----- .../js/spec/views/badge_view_spec.js | 114 ------ .../spec/views/learner_profile_view_spec.js | 24 +- .../js/spec/views/share_modal_view_spec.js | 63 ---- .../js/spec_helpers/helpers.js | 128 +------ .../js/views/badge_list_container.js | 35 -- .../js/views/badge_list_view.js | 65 ---- .../learner_profile/js/views/badge_view.js | 47 --- .../js/views/learner_profile_view.js | 22 +- .../js/views/share_modal_view.js | 56 --- .../templates/badge.underscore | 30 -- .../templates/badge_list.underscore | 4 - .../templates/badge_placeholder.underscore | 10 - .../templates/share_modal.underscore | 41 --- .../learner_profile/views/learner_profile.py | 6 - webpack-config/file-lists.js | 3 - xmodule/course_block.py | 8 - 72 files changed, 17 insertions(+), 3860 deletions(-) delete mode 100644 lms/djangoapps/badges/admin.py delete mode 100644 lms/djangoapps/badges/api/__init__.py delete mode 100644 lms/djangoapps/badges/api/serializers.py delete mode 100644 lms/djangoapps/badges/api/tests.py delete mode 100644 lms/djangoapps/badges/api/urls.py delete mode 100644 lms/djangoapps/badges/api/views.py delete mode 100644 lms/djangoapps/badges/backends/__init__.py delete mode 100644 lms/djangoapps/badges/backends/badgr.py delete mode 100644 lms/djangoapps/badges/backends/base.py delete mode 100644 lms/djangoapps/badges/backends/tests/__init__.py delete mode 100644 lms/djangoapps/badges/backends/tests/dummy_backend.py delete mode 100644 lms/djangoapps/badges/backends/tests/test_badgr_backend.py delete mode 100644 lms/djangoapps/badges/events/course_meta.py delete mode 100644 lms/djangoapps/badges/events/tests/__init__.py delete mode 100644 lms/djangoapps/badges/events/tests/test_course_complete.py delete mode 100644 lms/djangoapps/badges/events/tests/test_course_meta.py delete mode 100644 lms/djangoapps/badges/handlers.py delete mode 100644 lms/djangoapps/badges/service.py delete mode 100644 lms/djangoapps/badges/tests/__init__.py delete mode 100644 lms/djangoapps/badges/tests/factories.py delete mode 100644 lms/djangoapps/badges/tests/test_models.py delete mode 100644 lms/djangoapps/badges/utils.py delete mode 100644 openedx/features/learner_profile/static/learner_profile/js/models/badges_model.js delete mode 100644 openedx/features/learner_profile/static/learner_profile/js/spec/views/badge_list_container_spec.js delete mode 100644 openedx/features/learner_profile/static/learner_profile/js/spec/views/badge_list_view_spec.js delete mode 100644 openedx/features/learner_profile/static/learner_profile/js/spec/views/badge_view_spec.js delete mode 100644 openedx/features/learner_profile/static/learner_profile/js/spec/views/share_modal_view_spec.js delete mode 100644 openedx/features/learner_profile/static/learner_profile/js/views/badge_list_container.js delete mode 100644 openedx/features/learner_profile/static/learner_profile/js/views/badge_list_view.js delete mode 100644 openedx/features/learner_profile/static/learner_profile/js/views/badge_view.js delete mode 100644 openedx/features/learner_profile/static/learner_profile/js/views/share_modal_view.js delete mode 100644 openedx/features/learner_profile/static/learner_profile/templates/badge.underscore delete mode 100644 openedx/features/learner_profile/static/learner_profile/templates/badge_list.underscore delete mode 100644 openedx/features/learner_profile/static/learner_profile/templates/badge_placeholder.underscore delete mode 100644 openedx/features/learner_profile/static/learner_profile/templates/share_modal.underscore diff --git a/.github/workflows/unit-test-shards.json b/.github/workflows/unit-test-shards.json index 50e54ea22402..febd3dd878c2 100644 --- a/.github/workflows/unit-test-shards.json +++ b/.github/workflows/unit-test-shards.json @@ -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/", diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index f45130d9fa76..e5a0ab6a5b8f 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -126,11 +126,6 @@ def get_exclude_list_of_fields(cls, course_key): 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(): diff --git a/cms/envs/bok_choy.py b/cms/envs/bok_choy.py index f96183840c71..b6809dd48f0a 100644 --- a/cms/envs/bok_choy.py +++ b/cms/envs/bok_choy.py @@ -123,9 +123,6 @@ # 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' diff --git a/cms/envs/common.py b/cms/envs/common.py index 179eede100b1..eea4dae71688 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -321,9 +321,6 @@ # 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, diff --git a/common/djangoapps/util/tests/test_db.py b/common/djangoapps/util/tests/test_db.py index 4a16c2a20aa6..ee7825e87625 100644 --- a/common/djangoapps/util/tests/test_db.py +++ b/common/djangoapps/util/tests/test_db.py @@ -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): """ diff --git a/docs/lms-openapi.yaml b/docs/lms-openapi.yaml index 202b983212b3..9d95b947aef7 100644 --- a/docs/lms-openapi.yaml +++ b/docs/lms-openapi.yaml @@ -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 diff --git a/docs/references/docstrings/lms_index.rst b/docs/references/docstrings/lms_index.rst index 108a65cd39ea..43676bfdc1a6 100644 --- a/docs/references/docstrings/lms_index.rst +++ b/docs/references/docstrings/lms_index.rst @@ -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 diff --git a/lms/djangoapps/badges/admin.py b/lms/djangoapps/badges/admin.py deleted file mode 100644 index 096ac6f92ce0..000000000000 --- a/lms/djangoapps/badges/admin.py +++ /dev/null @@ -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) diff --git a/lms/djangoapps/badges/api/__init__.py b/lms/djangoapps/badges/api/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/lms/djangoapps/badges/api/serializers.py b/lms/djangoapps/badges/api/serializers.py deleted file mode 100644 index 2bcdd740eb33..000000000000 --- a/lms/djangoapps/badges/api/serializers.py +++ /dev/null @@ -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') diff --git a/lms/djangoapps/badges/api/tests.py b/lms/djangoapps/badges/api/tests.py deleted file mode 100644 index cadb30d11a9a..000000000000 --- a/lms/djangoapps/badges/api/tests.py +++ /dev/null @@ -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='') - ) diff --git a/lms/djangoapps/badges/api/urls.py b/lms/djangoapps/badges/api/urls.py deleted file mode 100644 index 95129fafcd1f..000000000000 --- a/lms/djangoapps/badges/api/urls.py +++ /dev/null @@ -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'), -] diff --git a/lms/djangoapps/badges/api/views.py b/lms/djangoapps/badges/api/views.py deleted file mode 100644 index b5b5b9cac4d4..000000000000 --- a/lms/djangoapps/badges/api/views.py +++ /dev/null @@ -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 diff --git a/lms/djangoapps/badges/apps.py b/lms/djangoapps/badges/apps.py index 4380a609b249..778c1b5790b2 100644 --- a/lms/djangoapps/badges/apps.py +++ b/lms/djangoapps/badges/apps.py @@ -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 diff --git a/lms/djangoapps/badges/backends/__init__.py b/lms/djangoapps/badges/backends/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/lms/djangoapps/badges/backends/badgr.py b/lms/djangoapps/badges/backends/badgr.py deleted file mode 100644 index 7569c41a2a16..000000000000 --- a/lms/djangoapps/badges/backends/badgr.py +++ /dev/null @@ -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) diff --git a/lms/djangoapps/badges/backends/base.py b/lms/djangoapps/badges/backends/base.py deleted file mode 100644 index 868ef0df7468..000000000000 --- a/lms/djangoapps/badges/backends/base.py +++ /dev/null @@ -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. - """ diff --git a/lms/djangoapps/badges/backends/tests/__init__.py b/lms/djangoapps/badges/backends/tests/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/lms/djangoapps/badges/backends/tests/dummy_backend.py b/lms/djangoapps/badges/backends/tests/dummy_backend.py deleted file mode 100644 index 042ffe9b24c6..000000000000 --- a/lms/djangoapps/badges/backends/tests/dummy_backend.py +++ /dev/null @@ -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) diff --git a/lms/djangoapps/badges/backends/tests/test_badgr_backend.py b/lms/djangoapps/badges/backends/tests/test_badgr_backend.py deleted file mode 100644 index 7e9f2d367a19..000000000000 --- a/lms/djangoapps/badges/backends/tests/test_badgr_backend.py +++ /dev/null @@ -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']} diff --git a/lms/djangoapps/badges/events/course_complete.py b/lms/djangoapps/badges/events/course_complete.py index 853465dfb8d6..654e3859ef2c 100644 --- a/lms/djangoapps/badges/events/course_complete.py +++ b/lms/djangoapps/badges/events/course_complete.py @@ -6,14 +6,9 @@ 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 diff --git a/lms/djangoapps/badges/events/course_meta.py b/lms/djangoapps/badges/events/course_meta.py deleted file mode 100644 index bef7b8dd79c2..000000000000 --- a/lms/djangoapps/badges/events/course_meta.py +++ /dev/null @@ -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) diff --git a/lms/djangoapps/badges/events/tests/__init__.py b/lms/djangoapps/badges/events/tests/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/lms/djangoapps/badges/events/tests/test_course_complete.py b/lms/djangoapps/badges/events/tests/test_course_complete.py deleted file mode 100644 index 582415d9c1d4..000000000000 --- a/lms/djangoapps/badges/events/tests/test_course_complete.py +++ /dev/null @@ -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) diff --git a/lms/djangoapps/badges/events/tests/test_course_meta.py b/lms/djangoapps/badges/events/tests/test_course_meta.py deleted file mode 100644 index a4c613852907..000000000000 --- a/lms/djangoapps/badges/events/tests/test_course_meta.py +++ /dev/null @@ -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 diff --git a/lms/djangoapps/badges/handlers.py b/lms/djangoapps/badges/handlers.py deleted file mode 100644 index d65448128a32..000000000000 --- a/lms/djangoapps/badges/handlers.py +++ /dev/null @@ -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) diff --git a/lms/djangoapps/badges/models.py b/lms/djangoapps/badges/models.py index 9af390174985..005cf1569ae1 100644 --- a/lms/djangoapps/badges/models.py +++ b/lms/djangoapps/badges/models.py @@ -3,24 +3,8 @@ """ -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("").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("").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("").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" diff --git a/lms/djangoapps/badges/service.py b/lms/djangoapps/badges/service.py deleted file mode 100644 index 2870ac7df4f9..000000000000 --- a/lms/djangoapps/badges/service.py +++ /dev/null @@ -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 diff --git a/lms/djangoapps/badges/tests/__init__.py b/lms/djangoapps/badges/tests/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/lms/djangoapps/badges/tests/factories.py b/lms/djangoapps/badges/tests/factories.py deleted file mode 100644 index f84053e3298e..000000000000 --- a/lms/djangoapps/badges/tests/factories.py +++ /dev/null @@ -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 diff --git a/lms/djangoapps/badges/tests/test_models.py b/lms/djangoapps/badges/tests/test_models.py deleted file mode 100644 index e57c2e1b2363..000000000000 --- a/lms/djangoapps/badges/tests/test_models.py +++ /dev/null @@ -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)) diff --git a/lms/djangoapps/badges/utils.py b/lms/djangoapps/badges/utils.py deleted file mode 100644 index 150834e04cdb..000000000000 --- a/lms/djangoapps/badges/utils.py +++ /dev/null @@ -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} diff --git a/lms/djangoapps/certificates/models.py b/lms/djangoapps/certificates/models.py index 7b5677a6909c..fa16112f3add 100644 --- a/lms/djangoapps/certificates/models.py +++ b/lms/djangoapps/certificates/models.py @@ -26,8 +26,6 @@ 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 Meta: 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. diff --git a/lms/djangoapps/certificates/tests/test_webview_views.py b/lms/djangoapps/certificates/tests/test_webview_views.py index 1fee63950289..7949b84f13b1 100644 --- a/lms/djangoapps/certificates/tests/test_webview_views.py +++ b/lms/djangoapps/certificates/tests/test_webview_views.py @@ -20,12 +20,6 @@ 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 @@ 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 @@ def setUp(self): ) CertificateHtmlViewConfigurationFactory.create() LinkedInAddToProfileConfigurationFactory.create() - CourseCompleteImageConfigurationFactory.create() def _add_course_certificates(self, count=1, signatory_count=0, is_active=True): """ @@ -533,32 +524,7 @@ def test_rendering_course_organization_data(self): self.assertContains(response, f'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 @@ def test_rendering_maximum_data(self): 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 @@ def test_rendering_maximum_data(self): 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 @@ def test_certificate_evidence_event_emitted(self): }, 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 - ) diff --git a/lms/djangoapps/certificates/tests/tests.py b/lms/djangoapps/certificates/tests/tests.py index ee3bd5c2b19c..a6b6362792b8 100644 --- a/lms/djangoapps/certificates/tests/tests.py +++ b/lms/djangoapps/certificates/tests/tests.py @@ -12,9 +12,8 @@ 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 @@ def test_course_milestone_collected(self): 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 diff --git a/lms/djangoapps/certificates/views/webview.py b/lms/djangoapps/certificates/views/webview.py index 4d99b7d7722d..28c7423948e0 100644 --- a/lms/djangoapps/certificates/views/webview.py +++ b/lms/djangoapps/certificates/views/webview.py @@ -15,7 +15,6 @@ 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.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()) diff --git a/lms/djangoapps/courseware/block_render.py b/lms/djangoapps/courseware/block_render.py index 650b4418b423..fc02e2662e67 100644 --- a/lms/djangoapps/courseware/block_render.py +++ b/lms/djangoapps/courseware/block_render.py @@ -40,8 +40,6 @@ 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 inner_get_block(block: XBlock) -> XBlock | None: '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(), diff --git a/lms/djangoapps/courseware/tests/test_block_render.py b/lms/djangoapps/courseware/tests/test_block_render.py index be1ebd5c05c0..668ce5171213 100644 --- a/lms/djangoapps/courseware/tests/test_block_render.py +++ b/lms/djangoapps/courseware/tests/test_block_render.py @@ -75,8 +75,6 @@ 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 @@ def test_get_set_tag(self): 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 """ diff --git a/lms/envs/bok_choy.yml b/lms/envs/bok_choy.yml index 0468867eb74b..f9d82f8d549d 100644 --- a/lms/envs/bok_choy.yml +++ b/lms/envs/bok_choy.yml @@ -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 diff --git a/lms/envs/common.py b/lms/envs/common.py index 0bad10728565..feebd335f453 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -696,19 +696,6 @@ # .. 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 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring 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 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring "level_of_education", 'social_links', 'time_zone', - - # Not an actual field, but used to signal whether badges should be public. - 'accomplishments_shared', ] ) diff --git a/lms/static/js/certificates/certificates.js b/lms/static/js/certificates/certificates.js index 6cc19796c488..e69de29bb2d1 100644 --- a/lms/static/js/certificates/certificates.js +++ b/lms/static/js/certificates/certificates.js @@ -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(); - }); -}); diff --git a/lms/static/js/learner_dashboard/spec/program_details_header_spec.js b/lms/static/js/learner_dashboard/spec/program_details_header_spec.js index 398632ebb46a..d28d8f0bd3ee 100644 --- a/lms/static/js/learner_dashboard/spec/program_details_header_spec.js +++ b/lms/static/js/learner_dashboard/spec/program_details_header_spec.js @@ -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'); }); diff --git a/lms/static/js/spec/student_account/helpers.js b/lms/static/js/spec/student_account/helpers.js index 9215bd4edcfe..e1dd11425d12 100644 --- a/lms/static/js/spec/student_account/helpers.js +++ b/lms/static/js/spec/student_account/helpers.js @@ -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, diff --git a/lms/static/js/student_account/models/user_account_model.js b/lms/static/js/student_account/models/user_account_model.js index d7ae96cc6cd0..2e3f9b034889 100644 --- a/lms/static/js/student_account/models/user_account_model.js +++ b/lms/static/js/student_account/models/user_account_model.js @@ -25,7 +25,6 @@ language_proficiencies: [], requires_parental_consent: true, profile_image: null, - accomplishments_shared: false, default_public_account_fields: [], extended_profile: [], secondary_email: '' diff --git a/lms/static/lms/js/spec/main.js b/lms/static/lms/js/spec/main.js index 795782948f0b..cd8668ddc443 100644 --- a/lms/static/lms/js/spec/main.js +++ b/lms/static/lms/js/spec/main.js @@ -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', diff --git a/lms/urls.py b/lms/urls.py index 8b314f9f1ea2..ca6a240eaba8 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -227,11 +227,6 @@ 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')), ] diff --git a/openedx/core/djangoapps/signals/signals.py b/openedx/core/djangoapps/signals/signals.py index d11caaa24d15..ca693b4d109b 100644 --- a/openedx/core/djangoapps/signals/signals.py +++ b/openedx/core/djangoapps/signals/signals.py @@ -11,7 +11,7 @@ # 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() diff --git a/openedx/core/djangoapps/user_api/accounts/serializers.py b/openedx/core/djangoapps/user_api/accounts/serializers.py index 75ecfa35bf2b..b17f479b452d 100644 --- a/openedx/core/djangoapps/user_api/accounts/serializers.py +++ b/openedx/core/djangoapps/user_api/accounts/serializers.py @@ -21,7 +21,6 @@ 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 @@ def to_representation(self, user): # lint-amnesty, pylint: disable=arguments-di 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 @@ def to_representation(self, user): # lint-amnesty, pylint: disable=arguments-di "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, diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_api.py b/openedx/core/djangoapps/user_api/accounts/tests/test_api.py index b856f3fc845b..569ec984662b 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_api.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_api.py @@ -625,7 +625,6 @@ def test_create_account(self): 'requires_parental_consent': True, 'language_proficiencies': [], 'account_privacy': PRIVATE_VISIBILITY, - 'accomplishments_shared': False, 'extended_profile': [], 'secondary_email': None, 'secondary_email_enabled': None, diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py index 63cb59cace5f..546b8afacc2b 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py @@ -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 @@ def _set_user_age_to_10_years(self, user): 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 @@ def _verify_full_shareable_account_response(self, response, account_privacy=None 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 @@ def _verify_full_account_response(self, response, requires_parental_consent=Fals 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 @@ def test_get_account_different_user_private(self): 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 @@ def verify_fields_visible_to_all_users(response): 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 @@ def verify_get_own_information(queries): 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)) diff --git a/openedx/core/djangoapps/user_api/accounts/views.py b/openedx/core/djangoapps/user_api/accounts/views.py index 83bdde4f442d..335e6178531f 100644 --- a/openedx/core/djangoapps/user_api/accounts/views.py +++ b/openedx/core/djangoapps/user_api/accounts/views.py @@ -250,9 +250,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. diff --git a/openedx/features/learner_profile/static/learner_profile/js/learner_profile_factory.js b/openedx/features/learner_profile/static/learner_profile/js/learner_profile_factory.js index 6419dd17703a..d4f81ae7f47c 100644 --- a/openedx/features/learner_profile/static/learner_profile/js/learner_profile_factory.js +++ b/openedx/features/learner_profile/static/learner_profile/js/learner_profile_factory.js @@ -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 }; }; }); diff --git a/openedx/features/learner_profile/static/learner_profile/js/models/badges_model.js b/openedx/features/learner_profile/static/learner_profile/js/models/badges_model.js deleted file mode 100644 index 42da19ef7677..000000000000 --- a/openedx/features/learner_profile/static/learner_profile/js/models/badges_model.js +++ /dev/null @@ -1,8 +0,0 @@ -(function(define) { - 'use strict'; - - define(['backbone'], function(Backbone) { - var BadgesModel = Backbone.Model.extend({}); - return BadgesModel; - }); -}).call(this, define || RequireJS.define); diff --git a/openedx/features/learner_profile/static/learner_profile/js/spec/learner_profile_factory_spec.js b/openedx/features/learner_profile/static/learner_profile/js/spec/learner_profile_factory_spec.js index f5b8f4bec6b5..0019c2fc136f 100644 --- a/openedx/features/learner_profile/static/learner_profile/js/spec/learner_profile_factory_spec.js +++ b/openedx/features/learner_profile/static/learner_profile/js/spec/learner_profile_factory_spec.js @@ -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, diff --git a/openedx/features/learner_profile/static/learner_profile/js/spec/views/badge_list_container_spec.js b/openedx/features/learner_profile/static/learner_profile/js/spec/views/badge_list_container_spec.js deleted file mode 100644 index 20342c4a6709..000000000000 --- a/openedx/features/learner_profile/static/learner_profile/js/spec/views/badge_list_container_spec.js +++ /dev/null @@ -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); - }); - }); -} -); diff --git a/openedx/features/learner_profile/static/learner_profile/js/spec/views/badge_list_view_spec.js b/openedx/features/learner_profile/static/learner_profile/js/spec/views/badge_list_view_spec.js deleted file mode 100644 index e8cfd32d4c38..000000000000 --- a/openedx/features/learner_profile/static/learner_profile/js/spec/views/badge_list_view_spec.js +++ /dev/null @@ -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); - }); - }); -} -); diff --git a/openedx/features/learner_profile/static/learner_profile/js/spec/views/badge_view_spec.js b/openedx/features/learner_profile/static/learner_profile/js/spec/views/badge_view_spec.js deleted file mode 100644 index 8ac88ae0b17e..000000000000 --- a/openedx/features/learner_profile/static/learner_profile/js/spec/views/badge_view_spec.js +++ /dev/null @@ -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); - }); - }); -} -); diff --git a/openedx/features/learner_profile/static/learner_profile/js/spec/views/learner_profile_view_spec.js b/openedx/features/learner_profile/static/learner_profile/js/spec/views/learner_profile_view_spec.js index 1797b0de05e7..21d2dd0f5df8 100644 --- a/openedx/features/learner_profile/static/learner_profile/js/spec/views/learner_profile_view_spec.js +++ b/openedx/features/learner_profile/static/learner_profile/js/spec/views/learner_profile_view_spec.js @@ -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); - }); }); }); diff --git a/openedx/features/learner_profile/static/learner_profile/js/spec/views/share_modal_view_spec.js b/openedx/features/learner_profile/static/learner_profile/js/spec/views/share_modal_view_spec.js deleted file mode 100644 index e3d15659ffa6..000000000000 --- a/openedx/features/learner_profile/static/learner_profile/js/spec/views/share_modal_view_spec.js +++ /dev/null @@ -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(); - }); - }); - } -); diff --git a/openedx/features/learner_profile/static/learner_profile/js/spec_helpers/helpers.js b/openedx/features/learner_profile/static/learner_profile/js/spec_helpers/helpers.js index 5d4a278907ca..e1369284a4b1 100644 --- a/openedx/features/learner_profile/static/learner_profile/js/spec_helpers/helpers.js +++ b/openedx/features/learner_profile/static/learner_profile/js/spec_helpers/helpers.js @@ -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 }; }); diff --git a/openedx/features/learner_profile/static/learner_profile/js/views/badge_list_container.js b/openedx/features/learner_profile/static/learner_profile/js/views/badge_list_container.js deleted file mode 100644 index ca68ca8fa9e8..000000000000 --- a/openedx/features/learner_profile/static/learner_profile/js/views/badge_list_container.js +++ /dev/null @@ -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); diff --git a/openedx/features/learner_profile/static/learner_profile/js/views/badge_list_view.js b/openedx/features/learner_profile/static/learner_profile/js/views/badge_list_view.js deleted file mode 100644 index c30ab9c94ad9..000000000000 --- a/openedx/features/learner_profile/static/learner_profile/js/views/badge_list_view.js +++ /dev/null @@ -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); diff --git a/openedx/features/learner_profile/static/learner_profile/js/views/badge_view.js b/openedx/features/learner_profile/static/learner_profile/js/views/badge_view.js deleted file mode 100644 index 854324916985..000000000000 --- a/openedx/features/learner_profile/static/learner_profile/js/views/badge_view.js +++ /dev/null @@ -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); diff --git a/openedx/features/learner_profile/static/learner_profile/js/views/learner_profile_view.js b/openedx/features/learner_profile/static/learner_profile/js/views/learner_profile_view.js index c75a22db14d6..74c0e6b819fb 100644 --- a/openedx/features/learner_profile/static/learner_profile/js/views/learner_profile_view.js +++ b/openedx/features/learner_profile/static/learner_profile/js/views/learner_profile_view.js @@ -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'))) { + if (this.showFullProfile()) { 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, diff --git a/openedx/features/learner_profile/static/learner_profile/js/views/share_modal_view.js b/openedx/features/learner_profile/static/learner_profile/js/views/share_modal_view.js deleted file mode 100644 index c018c3479ba0..000000000000 --- a/openedx/features/learner_profile/static/learner_profile/js/views/share_modal_view.js +++ /dev/null @@ -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); diff --git a/openedx/features/learner_profile/static/learner_profile/templates/badge.underscore b/openedx/features/learner_profile/static/learner_profile/templates/badge.underscore deleted file mode 100644 index 10901497270d..000000000000 --- a/openedx/features/learner_profile/static/learner_profile/templates/badge.underscore +++ /dev/null @@ -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> diff --git a/openedx/features/learner_profile/static/learner_profile/templates/badge_list.underscore b/openedx/features/learner_profile/static/learner_profile/templates/badge_list.underscore deleted file mode 100644 index 80c2cc2ca72b..000000000000 --- a/openedx/features/learner_profile/static/learner_profile/templates/badge_list.underscore +++ /dev/null @@ -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> diff --git a/openedx/features/learner_profile/static/learner_profile/templates/badge_placeholder.underscore b/openedx/features/learner_profile/static/learner_profile/templates/badge_placeholder.underscore deleted file mode 100644 index b7eb9976e8e0..000000000000 --- a/openedx/features/learner_profile/static/learner_profile/templates/badge_placeholder.underscore +++ /dev/null @@ -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> diff --git a/openedx/features/learner_profile/static/learner_profile/templates/share_modal.underscore b/openedx/features/learner_profile/static/learner_profile/templates/share_modal.underscore deleted file mode 100644 index b71e89823bad..000000000000 --- a/openedx/features/learner_profile/static/learner_profile/templates/share_modal.underscore +++ /dev/null @@ -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> \ No newline at end of file diff --git a/openedx/features/learner_profile/views/learner_profile.py b/openedx/features/learner_profile/views/learner_profile.py index e19e6853e8eb..6a3a251fde9a 100644 --- a/openedx/features/learner_profile/views/learner_profile.py +++ b/openedx/features/learner_profile/views/learner_profile.py @@ -11,7 +11,6 @@ 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 diff --git a/webpack-config/file-lists.js b/webpack-config/file-lists.js index ddb9cf4f7806..7167a6f5ddd8 100644 --- a/webpack-config/file-lists.js +++ b/webpack-config/file-lists.js @@ -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' diff --git a/xmodule/course_block.py b/xmodule/course_block.py index bb08e252b0f7..ae6d88b7b7b5 100644 --- a/xmodule/course_block.py +++ b/xmodule/course_block.py @@ -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=_(