Add a license field to libraries. (#25007)
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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'))
|
||||
)
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user