Skip to content
Snippets Groups Projects
Unverified Commit 73adc729 authored by Fox Piacenti's avatar Fox Piacenti Committed by GitHub
Browse files

Add a license field to libraries. (#25007)

parent 04744bc8
No related branches found
No related tags found
No related merge requests found
......@@ -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'))
)
# 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:
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment