From 73adc729d95c768a8df5ad45f756c78aeca00212 Mon Sep 17 00:00:00 2001 From: Fox Piacenti <fox@artconomy.com> Date: Mon, 5 Oct 2020 10:45:36 -0500 Subject: [PATCH] Add a license field to libraries. (#25007) --- .../core/djangoapps/content_libraries/api.py | 12 +++++++++++- .../djangoapps/content_libraries/constants.py | 19 +++++++++++++++++++ .../migrations/0004_contentlibrary_license.py | 18 ++++++++++++++++++ .../djangoapps/content_libraries/models.py | 6 +++++- .../content_libraries/serializers.py | 9 ++++++++- .../content_libraries/tests/base.py | 8 ++++++-- .../tests/test_content_libraries.py | 8 ++++++-- .../content_libraries/tests/test_runtime.py | 4 +++- .../djangoapps/content_libraries/views.py | 7 ++++++- 9 files changed, 82 insertions(+), 9 deletions(-) create mode 100644 openedx/core/djangoapps/content_libraries/migrations/0004_contentlibrary_license.py diff --git a/openedx/core/djangoapps/content_libraries/api.py b/openedx/core/djangoapps/content_libraries/api.py index 80355ea7ccd..73c0231aa3b 100644 --- a/openedx/core/djangoapps/content_libraries/api.py +++ b/openedx/core/djangoapps/content_libraries/api.py @@ -147,6 +147,7 @@ class ContentLibraryMetadata: # Allow any user with Studio access to view this library's content in # Studio, use it in their courses, and copy content out of this library. allow_public_read = attr.ib(False) + license = attr.ib("") class AccessLevel: @@ -307,6 +308,7 @@ def get_metadata_from_index(queryset, text_search=None): last_published=metadata[i].get('last_published'), has_unpublished_changes=metadata[i].get('has_unpublished_changes'), has_unpublished_deletes=metadata[i].get('has_unpublished_deletes'), + license=lib.license, ) for i, lib in enumerate(queryset) if metadata[i] is not None @@ -357,11 +359,13 @@ def get_library(library_key): allow_public_read=ref.allow_public_read, has_unpublished_changes=has_unpublished_changes, has_unpublished_deletes=has_unpublished_deletes, + license=ref.license, ) def create_library( collection_uuid, library_type, org, slug, title, description, allow_public_learning, allow_public_read, + library_license, ): """ Create a new content library. @@ -399,6 +403,7 @@ def create_library( bundle_uuid=bundle.uuid, allow_public_learning=allow_public_learning, allow_public_read=allow_public_read, + license=library_license, ) except IntegrityError: delete_bundle(bundle.uuid) @@ -415,6 +420,7 @@ def create_library( last_published=None, allow_public_learning=ref.allow_public_learning, allow_public_read=ref.allow_public_read, + license=library_license, ) @@ -490,9 +496,10 @@ def update_library( allow_public_learning=None, allow_public_read=None, library_type=None, + library_license=None, ): """ - Update a library's title or description. + Update a library's metadata (Slug cannot be changed as it would break IDs throughout the system.) A value of None means "don't change". @@ -522,6 +529,9 @@ def update_library( ) ref.type = library_type + changed = True + if library_license is not None: + ref.license = library_license changed = True if changed: ref.save() diff --git a/openedx/core/djangoapps/content_libraries/constants.py b/openedx/core/djangoapps/content_libraries/constants.py index 810ae37f863..0ae985ea2ce 100644 --- a/openedx/core/djangoapps/content_libraries/constants.py +++ b/openedx/core/djangoapps/content_libraries/constants.py @@ -14,3 +14,22 @@ LIBRARY_TYPES = ( (COMPLEX, _('Complex')), (PROBLEM, _('Problem')), ) + +# These are all the licenses we support so far. +ALL_RIGHTS_RESERVED = '' +CC_4_BY = 'CC:4.0:BY' +CC_4_BY_NC = 'CC:4.0:BY:NC' +CC_4_BY_NC_ND = 'CC:4.0:BY:NC:ND' +CC_4_BY_NC_SA = 'CC:4.0:BY:NC:SA' +CC_4_BY_ND = 'CC:4.0:BY:ND' +CC_4_BY_SA = 'CC:4.0:BY:SA' + +LICENSE_OPTIONS = ( + (ALL_RIGHTS_RESERVED, _('All Rights Reserved.')), + (CC_4_BY, _('Creative Commons Attribution 4.0')), + (CC_4_BY_NC, _('Creative Commons Attribution-NonCommercial 4.0')), + (CC_4_BY_NC_ND, _('Creative Commons Attribution-NonCommercial-NoDerivatives 4.0')), + (CC_4_BY_NC_SA, _('Creative Commons Attribution-NonCommercial-ShareAlike 4.0')), + (CC_4_BY_ND, _('Creative Commons Attribution-NoDerivatives 4.0')), + (CC_4_BY_SA, _('Creative Commons Attribution-ShareAlike 4.0')) +) diff --git a/openedx/core/djangoapps/content_libraries/migrations/0004_contentlibrary_license.py b/openedx/core/djangoapps/content_libraries/migrations/0004_contentlibrary_license.py new file mode 100644 index 00000000000..e571a30eb93 --- /dev/null +++ b/openedx/core/djangoapps/content_libraries/migrations/0004_contentlibrary_license.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.16 on 2020-09-16 21:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('content_libraries', '0003_contentlibrary_type'), + ] + + operations = [ + migrations.AddField( + model_name='contentlibrary', + name='license', + field=models.CharField(choices=[('', 'All Rights Reserved.'), ('CC:4.0:BY', 'Creative Commons Attribution 4.0'), ('CC:4.0:BY:NC', 'Creative Commons Attribution-NonCommercial 4.0'), ('CC:4.0:BY:NC:ND', 'Creative Commons Attribution-NonCommercial-NoDerivatives 4.0'), ('CC:4.0:BY:NC:SA', 'Creative Commons Attribution-NonCommercial-ShareAlike 4.0'), ('CC:4.0:BY:ND', 'Creative Commons Attribution-NoDerivatives 4.0'), ('CC:4.0:BY:SA', 'Creative Commons Attribution-ShareAlike 4.0')], default='', max_length=25), + ), + ] diff --git a/openedx/core/djangoapps/content_libraries/models.py b/openedx/core/djangoapps/content_libraries/models.py index 14014d8ace3..94b23189f26 100644 --- a/openedx/core/djangoapps/content_libraries/models.py +++ b/openedx/core/djangoapps/content_libraries/models.py @@ -7,7 +7,10 @@ from django.core.exceptions import ValidationError from django.db import models from django.utils.translation import ugettext_lazy as _ from opaque_keys.edx.locator import LibraryLocatorV2 -from openedx.core.djangoapps.content_libraries.constants import LIBRARY_TYPES, COMPLEX +from openedx.core.djangoapps.content_libraries.constants import ( + LIBRARY_TYPES, COMPLEX, LICENSE_OPTIONS, + ALL_RIGHTS_RESERVED, +) from organizations.models import Organization import six @@ -47,6 +50,7 @@ class ContentLibrary(models.Model): slug = models.SlugField(allow_unicode=True) bundle_uuid = models.UUIDField(unique=True, null=False) type = models.CharField(max_length=25, default=COMPLEX, choices=LIBRARY_TYPES) + license = models.CharField(max_length=25, default=ALL_RIGHTS_RESERVED, choices=LICENSE_OPTIONS) # How is this library going to be used? allow_public_learning = models.BooleanField( diff --git a/openedx/core/djangoapps/content_libraries/serializers.py b/openedx/core/djangoapps/content_libraries/serializers.py index 9c8a0eaf24f..52978eeb8bb 100644 --- a/openedx/core/djangoapps/content_libraries/serializers.py +++ b/openedx/core/djangoapps/content_libraries/serializers.py @@ -5,7 +5,12 @@ Serializers for the content libraries REST API from django.core.validators import validate_unicode_slug from rest_framework import serializers -from openedx.core.djangoapps.content_libraries.constants import LIBRARY_TYPES, COMPLEX +from openedx.core.djangoapps.content_libraries.constants import ( + LIBRARY_TYPES, + COMPLEX, + ALL_RIGHTS_RESERVED, + LICENSE_OPTIONS, +) from openedx.core.djangoapps.content_libraries.models import ContentLibraryPermission from openedx.core.lib import blockstore_api @@ -36,6 +41,7 @@ class ContentLibraryMetadataSerializer(serializers.Serializer): allow_public_read = serializers.BooleanField(default=False) has_unpublished_changes = serializers.BooleanField(read_only=True) has_unpublished_deletes = serializers.BooleanField(read_only=True) + license = serializers.ChoiceField(choices=LICENSE_OPTIONS, default=ALL_RIGHTS_RESERVED) class ContentLibraryUpdateSerializer(serializers.Serializer): @@ -48,6 +54,7 @@ class ContentLibraryUpdateSerializer(serializers.Serializer): allow_public_learning = serializers.BooleanField() allow_public_read = serializers.BooleanField() type = serializers.ChoiceField(choices=LIBRARY_TYPES) + license = serializers.ChoiceField(choices=LICENSE_OPTIONS) class ContentLibraryPermissionLevelSerializer(serializers.Serializer): diff --git a/openedx/core/djangoapps/content_libraries/tests/base.py b/openedx/core/djangoapps/content_libraries/tests/base.py index 6788c423492..dfb19608192 100644 --- a/openedx/core/djangoapps/content_libraries/tests/base.py +++ b/openedx/core/djangoapps/content_libraries/tests/base.py @@ -16,7 +16,7 @@ from search.search_engine_base import SearchEngine from student.tests.factories import UserFactory from openedx.core.djangoapps.content_libraries.libraries_index import MAX_SIZE -from openedx.core.djangoapps.content_libraries.constants import COMPLEX +from openedx.core.djangoapps.content_libraries.constants import COMPLEX, ALL_RIGHTS_RESERVED from openedx.core.djangolib.testing.utils import skip_unless_cms from openedx.core.lib import blockstore_api @@ -167,7 +167,10 @@ class ContentLibrariesRestApiTest(APITestCase): yield self.client = old_client # pylint: disable=attribute-defined-outside-init - def _create_library(self, slug, title, description="", org=None, library_type=COMPLEX, expect_response=200): + def _create_library( + self, slug, title, description="", org=None, library_type=COMPLEX, + license_type=ALL_RIGHTS_RESERVED, expect_response=200, + ): """ Create a library """ if org is None: org = self.organization.short_name @@ -177,6 +180,7 @@ class ContentLibrariesRestApiTest(APITestCase): "title": title, "description": description, "type": library_type, + "license": license_type, "collection_uuid": str(self.collection.uuid), }, expect_response) diff --git a/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py b/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py index 4edd0b56bda..ca54adf21bb 100644 --- a/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py +++ b/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py @@ -12,7 +12,7 @@ from mock import patch from organizations.models import Organization from openedx.core.djangoapps.content_libraries.tests.base import ContentLibrariesRestApiTest, elasticsearch_test -from openedx.core.djangoapps.content_libraries.constants import VIDEO, COMPLEX, PROBLEM +from openedx.core.djangoapps.content_libraries.constants import VIDEO, COMPLEX, PROBLEM, CC_4_BY, ALL_RIGHTS_RESERVED from student.tests.factories import UserFactory @@ -47,7 +47,9 @@ class ContentLibrariesTest(ContentLibrariesRestApiTest): Test Create, Read, Update, and Delete of a Content Library """ # Create: - lib = self._create_library(slug="lib-crud", title="A Test Library", description="Just Testing") + lib = self._create_library( + slug="lib-crud", title="A Test Library", description="Just Testing", license_type=CC_4_BY, + ) expected_data = { "id": "lib:CL-TEST:lib-crud", "org": "CL-TEST", @@ -56,6 +58,7 @@ class ContentLibrariesTest(ContentLibrariesRestApiTest): "description": "Just Testing", "version": 0, "type": COMPLEX, + "license": CC_4_BY, "has_unpublished_changes": False, "has_unpublished_deletes": False, } @@ -96,6 +99,7 @@ class ContentLibrariesTest(ContentLibrariesRestApiTest): "version": 0, "has_unpublished_changes": False, "has_unpublished_deletes": False, + "license": ALL_RIGHTS_RESERVED, } self.assertDictContainsEntries(lib, expected_data) diff --git a/openedx/core/djangoapps/content_libraries/tests/test_runtime.py b/openedx/core/djangoapps/content_libraries/tests/test_runtime.py index 4551fc71982..69e18fd26e6 100644 --- a/openedx/core/djangoapps/content_libraries/tests/test_runtime.py +++ b/openedx/core/djangoapps/content_libraries/tests/test_runtime.py @@ -19,7 +19,7 @@ from openedx.core.djangoapps.content_libraries.tests.base import ( URL_BLOCK_METADATA_URL, ) from openedx.core.djangoapps.content_libraries.tests.user_state_block import UserStateTestBlock -from openedx.core.djangoapps.content_libraries.constants import COMPLEX +from openedx.core.djangoapps.content_libraries.constants import COMPLEX, ALL_RIGHTS_RESERVED, CC_4_BY from openedx.core.djangoapps.dark_lang.models import DarkLangConfig from openedx.core.djangoapps.xblock import api as xblock_api from openedx.core.djangolib.testing.utils import skip_unless_lms, skip_unless_cms @@ -55,6 +55,7 @@ class ContentLibraryContentTestMixin(object): description="", allow_public_learning=True, allow_public_read=False, + library_license=ALL_RIGHTS_RESERVED, ) @@ -89,6 +90,7 @@ class ContentLibraryRuntimeTest(ContentLibraryContentTestMixin, TestCase): library_type=COMPLEX, allow_public_learning=True, allow_public_read=False, + library_license=CC_4_BY, ) unit_block2_key = library_api.create_library_block(library2.key, "unit", "u1").usage_key library_api.create_library_block_child(unit_block2_key, "problem", "p1") diff --git a/openedx/core/djangoapps/content_libraries/views.py b/openedx/core/djangoapps/content_libraries/views.py index 91cdf2ac00a..90154005612 100644 --- a/openedx/core/djangoapps/content_libraries/views.py +++ b/openedx/core/djangoapps/content_libraries/views.py @@ -152,8 +152,10 @@ class LibraryRootView(APIView): serializer = ContentLibraryMetadataSerializer(data=request.data) serializer.is_valid(raise_exception=True) data = dict(serializer.validated_data) - # Converting this over because using the reserved name 'type' would shadow the built-in definition elsewhere. + # Converting this over because using the reserved names 'type' and 'license' would shadow the built-in + # definitions elsewhere. data['library_type'] = data.pop('type') + data['library_license'] = data.pop('license') # Get the organization short_name out of the "key.org" pseudo-field that the serializer added: org_name = data["key"]["org"] # Move "slug" out of the "key.slug" pseudo-field that the serializer added: @@ -196,8 +198,11 @@ class LibraryDetailsView(APIView): serializer = ContentLibraryUpdateSerializer(data=request.data, partial=True) serializer.is_valid(raise_exception=True) data = dict(serializer.validated_data) + # Prevent ourselves from shadowing global names. if 'type' in data: data['library_type'] = data.pop('type') + if 'license' in data: + data['library_license'] = data.pop('license') try: api.update_library(key, **data) except api.IncompatibleTypesError as err: -- GitLab