diff --git a/openedx/core/djangoapps/content_libraries/api.py b/openedx/core/djangoapps/content_libraries/api.py index 80355ea7ccd03d9a0767ed5c223bd3c704b78fe2..73c0231aa3be4cd9ac89c7b513458273ff1a89fe 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 810ae37f863f922e64dcba186842068b107212c0..0ae985ea2ce3fdeb2ce7a8e8d7fb929101e4fd2a 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 0000000000000000000000000000000000000000..e571a30eb936afca773cbfdc24ce42a048f97a2e --- /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 14014d8ace377a83f0c9de16da616db37998ac51..94b23189f26c5470aa4a08e004ac7262ac64ba24 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 9c8a0eaf24f4a4e833ed5026711195be7f965233..52978eeb8bbdfe3cdeaa79772d67b56272ddf3d2 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 6788c423492d5234403027868c5bda2e9e3917b0..dfb196081921613b6ecd18e9fa4f52afe08c8408 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 4edd0b56bdab5bd1b80425e96d929f38fe9b1ffb..ca54adf21bb16cb9fcac8b4634b4e803258e1523 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 4551fc719824966553fd8a84585334c8ec73dc77..69e18fd26e6d10ce79c96b15ec353132775175bc 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 91cdf2ac00a0cc82ac1c8a3812e071ed053a08fe..9015400561268cc2919d56e419b785573d224ba6 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: