Skip to content

Commit

Permalink
[#2478] Default licenses more specific, and related deprecation mecha…
Browse files Browse the repository at this point in the history
…nism

* Default is three well-known CC licences with version numbers.
* License options offered is now a subset of all of them, to allow
  deprecation of licenses. Config options added, plus logic in helper.
* Default licenses now in a JSON file for simple customization by
  copying it. Added extractor to ensure titles are translated.
* Simplified configuring a file with licences in - no more weird
  file:/// to work out.
* Form CSS marginally improved with a bigger select box for the long
  license names. Adjusted some other sizings in sympathy.
  • Loading branch information
David Read committed Jun 22, 2015
1 parent 96e9f20 commit 51eb38c
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 247 deletions.
19 changes: 19 additions & 0 deletions ckan/lib/extract.py
@@ -1,4 +1,6 @@
import re
import json

from genshi.filters.i18n import extract as extract_genshi
from jinja2.ext import babel_extract as extract_jinja2
import lib.jinja_extensions
Expand Down Expand Up @@ -49,3 +51,20 @@ def extract_ckan(fileobj, *args, **kw):
# we've eaten the file so we need to get back to the start
fileobj.seek(0)
return output


def extract_licenses(fileobj, keywords, comment_tags, options):
''' Extract the translatable strings from a licenses JSON file.
:return: an iterator over ``(lineno, funcname, message, comments)``
tuples
'''
licenses_json = fileobj.read()
licenses = json.loads(licenses_json)
licenses_list = licenses.keys() if isinstance(licenses, dict) \
else licenses
for i, license in enumerate(licenses_list):
if license.keys() == ['comment']:
continue
yield (i, 'gettext', license['title'],
['License title (%s)' % license['id']])
44 changes: 44 additions & 0 deletions ckan/lib/helpers.py
Expand Up @@ -2032,6 +2032,49 @@ def get_organization(org=None, include_datasets=False):
except (NotFound, ValidationError, NotAuthorized):
return {}


def license_options(existing_license_id=None):
'''Returns [(l.title, l.id), ...] for the licenses configured to be
offered. Always includes the existing_license_id, if supplied.
'''
register = model.Package.get_license_register()
all_license_ids = register.keys()

def check_license_ids(license_ids):
for license_id in license_ids:
assert license_id in all_license_ids, \
'License "%s" not in the register: %s' % (license_id,
all_license_ids)
if config.get('ckan.licenses_offered'):
license_ids = config.get('ckan.licenses_offered').split()
check_license_ids(license_ids)
# use the ordering specified in the config
else:
if config.get('ckan.licenses_offered_exclusions'):
exclude_ids = config.get('ckan.licenses_offered_exclusions')\
.split()
check_license_ids(exclude_ids)
else:
# default is to exclude these ones from older CKAN versions
exclude_ids = (
'odc-pddl', 'odc-odbl', 'odc-by', 'cc-zero', 'cc-by',
'cc-by-sa', 'gfdl', 'other-open', 'other-pd', 'other-at',
'uk-ogl', 'cc-nc', 'other-nc', 'other-closed')
sorted_licenses = sorted(register.values(), key=lambda x: x.title)
# put notspecified at the end
notspecified = register.get('notspecified')
if notspecified and notspecified in sorted_licenses:
sorted_licenses.remove(notspecified)
sorted_licenses.append(notspecified)
license_ids = [license.id for license in sorted_licenses
if license.id not in exclude_ids]
if existing_license_id and existing_license_id not in license_ids:
license_ids.insert(0, existing_license_id)
return [
(license_id,
register[license_id].title if license_id in register else license_id)
for license_id in license_ids]

# these are the functions that will end up in `h` template helpers
__allowed_functions__ = [
# functions defined in ckan.lib.helpers
Expand Down Expand Up @@ -2150,4 +2193,5 @@ def get_organization(org=None, include_datasets=False):
'urlencode',
'check_config_permission',
'view_resource_url',
'license_options',
]
255 changes: 36 additions & 219 deletions ckan/model/license.py
@@ -1,11 +1,12 @@
import datetime
import urllib2
import re
import os

from pylons import config
from paste.deploy.converters import asbool

from ckan.common import _, json
from ckan.common import json
import ckan.lib.maintain as maintain

log = __import__('logging').getLogger(__name__)
Expand Down Expand Up @@ -82,30 +83,35 @@ class LicenseRegister(object):
"""Dictionary-like interface to a group of licenses."""

def __init__(self):
group_url = config.get('licenses_group_url', None)
if group_url:
self.load_licenses(group_url)
filepath = config.get('ckan.licenses_file')
url = config.get('ckan.licenses_url') or \
config.get('licenses_group_url')
if filepath:
self.load_licenses_from_file(filepath)
elif url:
self.load_licenses_from_url(url)
else:
default_license_list = [
LicenseNotSpecified(),
LicenseOpenDataCommonsPDDL(),
LicenseOpenDataCommonsOpenDatabase(),
LicenseOpenDataAttribution(),
LicenseCreativeCommonsZero(),
LicenseCreativeCommonsAttribution(),
LicenseCreativeCommonsAttributionShareAlike(),
LicenseGNUFreeDocument(),
LicenseOtherOpen(),
LicenseOtherPublicDomain(),
LicenseOtherAttribution(),
LicenseOpenGovernment(),
LicenseCreativeCommonsNonCommercial(),
LicenseOtherNonCommercial(),
LicenseOtherClosed(),
]
self._create_license_list(default_license_list)
default_filepath = os.path.abspath(
os.path.join(os.path.dirname(__file__),
'..', 'config',
'licenses_default.json'))
self.load_licenses_from_file(default_filepath)

def load_licenses(self, license_url):
def load_licenses_from_file(self, filepath):
try:
with open(filepath, 'rb') as f:
content = f.read()
except IOError, inst:
msg = "Couldn't open license file %r: %s" % (filepath, inst)
raise Exception(msg)
try:
license_data = json.loads(content)
except ValueError, inst:
msg = "Couldn't read JSON in license file %r: %s" % (filepath, inst)
raise Exception(msg)
self._create_license_list(license_data, filepath)

def load_licenses_from_url(self, license_url):
try:
response = urllib2.urlopen(license_url)
response_body = response.read()
Expand All @@ -119,13 +125,16 @@ def load_licenses(self, license_url):
raise Exception(inst)
self._create_license_list(license_data, license_url)

def _create_license_list(self, license_data, license_url=''):
def _create_license_list(self, license_data, location=''):
if isinstance(license_data, dict):
self.licenses = [License(entity) for entity in license_data.values()]
self.licenses = [License(entity)
for entity in license_data.values()
if entity.keys() != ['comment']]
elif isinstance(license_data, list):
self.licenses = [License(entity) for entity in license_data]
self.licenses = [License(entity) for entity in license_data
if entity.keys() != ['comment']]
else:
msg = "Licenses at %s must be dictionary or list" % license_url
msg = "Licenses at %s must be a list or dictionary" % location
raise ValueError(msg)

def __getitem__(self, key, default=Exception):
Expand Down Expand Up @@ -155,195 +164,3 @@ def __iter__(self):
def __len__(self):
return len(self.licenses)


class DefaultLicense(dict):
''' The license was a dict but this did not allow translation of the
title. This is a slightly changed dict that allows us to have the title
as a property and so translated. '''

domain_content = False
domain_data = False
domain_software = False
family = ''
is_generic = False
od_conformance = 'not reviewed'
osd_conformance = 'not reviewed'
maintainer = ''
status = 'active'
url = ''
title = ''
id = ''

keys = ['domain_content',
'id',
'domain_data',
'domain_software',
'family',
'is_generic',
'od_conformance',
'osd_conformance',
'maintainer',
'status',
'url',
'title']

def __getitem__(self, key):
''' behave like a dict but get from attributes '''
if key in self.keys:
value = getattr(self, key)
if isinstance(value, str):
return unicode(value)
else:
return value
else:
raise KeyError()

def copy(self):
''' create a dict of the license used by the licenses api '''
out = {}
for key in self.keys:
out[key] = unicode(getattr(self, key))
return out

class LicenseNotSpecified(DefaultLicense):
id = "notspecified"
is_generic = True

@property
def title(self):
return _("License not specified")

class LicenseOpenDataCommonsPDDL(DefaultLicense):
domain_data = True
id = "odc-pddl"
od_conformance = 'approved'
url = "http://www.opendefinition.org/licenses/odc-pddl"

@property
def title(self):
return _("Open Data Commons Public Domain Dedication and License (PDDL)")

class LicenseOpenDataCommonsOpenDatabase(DefaultLicense):
domain_data = True
id = "odc-odbl"
od_conformance = 'approved'
url = "http://www.opendefinition.org/licenses/odc-odbl"

@property
def title(self):
return _("Open Data Commons Open Database License (ODbL)")

class LicenseOpenDataAttribution(DefaultLicense):
domain_data = True
id = "odc-by"
od_conformance = 'approved'
url = "http://www.opendefinition.org/licenses/odc-by"

@property
def title(self):
return _("Open Data Commons Attribution License")

class LicenseCreativeCommonsZero(DefaultLicense):
domain_content = True
domain_data = True
id = "cc-zero"
od_conformance = 'approved'
url = "http://www.opendefinition.org/licenses/cc-zero"

@property
def title(self):
return _("Creative Commons CCZero")

class LicenseCreativeCommonsAttribution(DefaultLicense):
id = "cc-by"
od_conformance = 'approved'
url = "http://www.opendefinition.org/licenses/cc-by"

@property
def title(self):
return _("Creative Commons Attribution")

class LicenseCreativeCommonsAttributionShareAlike(DefaultLicense):
domain_content = True
id = "cc-by-sa"
od_conformance = 'approved'
url = "http://www.opendefinition.org/licenses/cc-by-sa"

@property
def title(self):
return _("Creative Commons Attribution Share-Alike")

class LicenseGNUFreeDocument(DefaultLicense):
domain_content = True
id = "gfdl"
od_conformance = 'approved'
url = "http://www.opendefinition.org/licenses/gfdl"
@property
def title(self):
return _("GNU Free Documentation License")

class LicenseOtherOpen(DefaultLicense):
domain_content = True
id = "other-open"
is_generic = True
od_conformance = 'approved'

@property
def title(self):
return _("Other (Open)")

class LicenseOtherPublicDomain(DefaultLicense):
domain_content = True
id = "other-pd"
is_generic = True
od_conformance = 'approved'

@property
def title(self):
return _("Other (Public Domain)")

class LicenseOtherAttribution(DefaultLicense):
domain_content = True
id = "other-at"
is_generic = True
od_conformance = 'approved'

@property
def title(self):
return _("Other (Attribution)")

class LicenseOpenGovernment(DefaultLicense):
domain_content = True
id = "uk-ogl"
od_conformance = 'approved'
# CS: bad_spelling ignore
url = "http://reference.data.gov.uk/id/open-government-licence"

@property
def title(self):
# CS: bad_spelling ignore
return _("UK Open Government Licence (OGL)")

class LicenseCreativeCommonsNonCommercial(DefaultLicense):
id = "cc-nc"
url = "http://creativecommons.org/licenses/by-nc/2.0/"

@property
def title(self):
return _("Creative Commons Non-Commercial (Any)")

class LicenseOtherNonCommercial(DefaultLicense):
id = "other-nc"
is_generic = True

@property
def title(self):
return _("Other (Non-Commercial)")

class LicenseOtherClosed(DefaultLicense):
id = "other-closed"
is_generic = True

@property
def title(self):
return _("Other (Not Open)")
4 changes: 2 additions & 2 deletions ckan/public/base/less/forms.less
Expand Up @@ -170,11 +170,11 @@ textarea {

@media (min-width: 980px) {
.form-horizontal .info-block {
padding: 6px 0 6px 25px;
padding: 0px 0 6px 25px;
}
.form-horizontal .info-inline {
float: right;
width: 265px;
width: 200px;
margin-top: 0;
padding-bottom: 0;
}
Expand Down

0 comments on commit 51eb38c

Please sign in to comment.