diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py index 3a89d7b3c1b..ce75e0fdd24 100644 --- a/ckan/controllers/package.py +++ b/ckan/controllers/package.py @@ -200,13 +200,23 @@ def _content_type_for_format(self, fmt): it accurately. TextTemplate must be used for non-xml templates whilst all that are some sort of XML should use MarkupTemplate. """ - from genshi.template import MarkupTemplate, TextTemplate + from genshi.template import MarkupTemplate + from genshi.template.text import NewTextTemplate + types = { - "html": ("text/html; charset=utf-8", MarkupTemplate), - "rdf" : ("application/rdf+xml; charset=utf-8", MarkupTemplate), + "html": ("text/html; charset=utf-8", MarkupTemplate, 'html'), + "rdf" : ("application/rdf+xml; charset=utf-8", MarkupTemplate, 'rdf'), + "n3" : ("text/n3; charset=utf-8", NewTextTemplate, 'n3'), + "application/rdf+xml" : ("application/rdf+xml; charset=utf-8", MarkupTemplate,'rdf'), + "text/n3": ("text/n3; charset=utf-8", NewTextTemplate, 'n3'), } + # Check the accept header first + accept = request.headers.get('Accept', '') + if accept and accept in types: + return types[accept][0], types[accept][2], types[accept][1] + if fmt in types: - return types[fmt][0], fmt, types[fmt][1] + return types[fmt][0], types[fmt][2], types[fmt][1] return None, "html", (types["html"][1]) @@ -219,6 +229,9 @@ def read(self, id, format='html'): ctype = "text/html; charset=utf-8" id = "%s.%s" % (id, format) format = 'html' + else: + format = extension + response.headers['Content-Type'] = ctype package_type = self._get_package_type(id.split('@')[0]) @@ -268,14 +281,6 @@ def read(self, id, format='html'): ckan.logic.action.get.package_activity_list_html(context, {'id': c.current_package_id}) - if config.get('rdf_packages'): - accept_header = request.headers.get('Accept', '*/*') - for content_type, exts in negotiate(autoneg_cfg, accept_header): - if "html" not in exts: - rdf_url = '%s%s.%s' % (config['rdf_packages'], c.pkg.id, exts[0]) - redirect(rdf_url, code=303) - break - PackageSaver().render_package(c.pkg_dict, context) template = self._read_template( package_type ) @@ -529,22 +534,11 @@ def _get_package_type(self, id): based on the package's type name (type). The plugin found will be returned, or None if there is no plugin associated with the type. - - Uses a minimal context to do so. The main use of this method - is for figuring out which plugin to delegate to. - - aborts if an exception is raised. """ - context = {'model': model, 'session': model.Session, - 'user': c.user or c.author} - try: - data = get_action('package_show')(context, {'id': id}) - except NotFound: - abort(404, _('Dataset not found')) - except NotAuthorized: - abort(401, _('Unauthorized to read package %s') % id) - - return data.get('type', 'package') + pkg = model.Package.get(id) + if pkg: + return pkg.type or 'package' + return None def _save_new(self, context, package_type=None): from ckan.lib.search import SearchIndexError diff --git a/ckan/controllers/storage.py b/ckan/controllers/storage.py index 56218d945b5..34af131086c 100644 --- a/ckan/controllers/storage.py +++ b/ckan/controllers/storage.py @@ -48,6 +48,9 @@ def fix_stupid_pylons_encoding(data): def create_pairtree_marker(folder): """ Creates the pairtree marker for tests if it doesn't exist """ + if not folder[:-1] == '/': + folder = folder + '/' + directory = os.path.dirname(folder) if not os.path.exists(directory): os.makedirs(directory) diff --git a/ckan/lib/base.py b/ckan/lib/base.py index 2c346d8acf8..61ffa2e52a4 100644 --- a/ckan/lib/base.py +++ b/ckan/lib/base.py @@ -16,6 +16,7 @@ from pylons.i18n import _, ungettext, N_, gettext from pylons.templating import cached_template, pylons_globals from genshi.template import MarkupTemplate +from genshi.template.text import NewTextTemplate from webhelpers.html import literal import ckan @@ -39,14 +40,14 @@ def abort(status_code=None, detail='', headers=None, comment=None): # #1267 Convert detail to plain text, since WebOb 0.9.7.1 (which comes # with Lucid) causes an exception when unicode is received. detail = detail.encode('utf8') - return _abort(status_code=status_code, + return _abort(status_code=status_code, detail=detail, - headers=headers, + headers=headers, comment=comment) -def render(template_name, extra_vars=None, cache_key=None, cache_type=None, +def render(template_name, extra_vars=None, cache_key=None, cache_type=None, cache_expire=None, method='xhtml', loader_class=MarkupTemplate): - + def render_template(): globs = extra_vars or {} globs.update(pylons_globals()) @@ -60,21 +61,23 @@ def render_template(): template = globs['app_globals'].genshi_loader.load(template_name, cls=loader_class) stream = template.generate(**globs) - + for item in PluginImplementations(IGenshiStreamFilter): stream = item.filter(stream) - + + if loader_class == NewTextTemplate: + return literal(stream.render(method="text", encoding=None)) return literal(stream.render(method=method, encoding=None, strip_whitespace=False)) - + if 'Pragma' in response.headers: del response.headers["Pragma"] if cache_key is not None or cache_type is not None: - response.headers["Cache-Control"] = "public" - + response.headers["Cache-Control"] = "public" + if cache_expire is not None: response.headers["Cache-Control"] = "max-age=%s, must-revalidate" % cache_expire - - return cached_template(template_name, render_template, cache_key=cache_key, + + return cached_template(template_name, render_template, cache_key=cache_key, cache_type=cache_type, cache_expire=cache_expire) #, ns_options=('method'), method=method) @@ -136,7 +139,7 @@ def __call__(self, environ, start_response): """Invoke the Controller""" # WSGIController.__call__ dispatches to the Controller method # the request is routed to. This routing information is - # available in environ['pylons.routes_dict'] + # available in environ['pylons.routes_dict'] try: return WSGIController.__call__(self, environ, start_response) finally: @@ -260,13 +263,13 @@ def _get_timing_cache_path(self): return path @classmethod - def _get_user_editable_groups(cls): + def _get_user_editable_groups(cls): if not hasattr(c, 'user'): c.user = model.PSEUDO_USER__VISITOR import ckan.authz # Todo: Move import to top of this file? - groups = ckan.authz.Authorizer.authorized_query(c.user, model.Group, + groups = ckan.authz.Authorizer.authorized_query(c.user, model.Group, action=model.Action.EDIT).all() - return [g for g in groups if g.state==model.State.ACTIVE] + return [g for g in groups if g.state==model.State.ACTIVE] def _get_package_dict(self, *args, **kwds): import ckan.forms @@ -325,7 +328,7 @@ def _handle_update_of_authz(self, domain_object): update_or_add = 'add' else: user_or_authgroup = None - update_or_add = None + update_or_add = None # Work out what role checkboxes are checked or unchecked checked_roles = [ box_id for (box_id, value) in request.params.items() \ diff --git a/ckan/templates/layout_base.html b/ckan/templates/layout_base.html index 51fa8088c1c..61ba69c2f14 100644 --- a/ckan/templates/layout_base.html +++ b/ckan/templates/layout_base.html @@ -31,7 +31,7 @@ - + diff --git a/ckan/templates/package/read.n3 b/ckan/templates/package/read.n3 new file mode 100644 index 00000000000..911e83229cb --- /dev/null +++ b/ckan/templates/package/read.n3 @@ -0,0 +1,44 @@ +@prefix : . +@prefix dcat: . +@prefix dct: . +@prefix foaf: . +@prefix owl: . +@prefix rdf: . + +<${ g.site_url + h.url_for(controller='package',action='read',id=c.pkg_dict['name'])}> a dcat:Dataset; + dct:description "${c.pkg_dict['notes']}"; + dct:identifier "${c.pkg_dict['name']}"; + dct:relation [ + rdf:value ""; + :label "change_note" ], + [ + rdf:value ""; + :label "definition_note" ], + [ + rdf:value ""; + :label "editorial_note" ], + [ + rdf:value ""; + :label "example_note" ], + [ + rdf:value ""; + :label "history_note" ], + [ + rdf:value ""; + :label "scope_note" ], + [ + rdf:value ""; + :label "skos_note" ], + [ + rdf:value ""; + :label "temporal_granularity" ], + [ + rdf:value ""; + :label "type_of_dataset" ], + [ + rdf:value ""; + :label "update_frequency" ]; + dct:title "${c.pkg_dict['title']}"; + :label "${c.pkg_dict['name']}"; + = ; + foaf:homepage . \ No newline at end of file diff --git a/ckan/tests/functional/test_autoneg.py b/ckan/tests/functional/test_autoneg.py index 7732f7cb055..0d1181498a1 100644 --- a/ckan/tests/functional/test_autoneg.py +++ b/ckan/tests/functional/test_autoneg.py @@ -59,20 +59,18 @@ def test_html_rdf(self): assert response.status == 200, response.status content_type = response.header("Content-Type") assert "html" in content_type, content_type - + def test_rdfxml(self): url = url_for(controller='package', action='read', id='annakarenina') response = self.app.get(url, headers={"Accept": "application/rdf+xml"}) - assert response.status == 303, response.status - location = response.header("Location") - assert location.endswith(".rdf"), location - assert location.startswith('http://test.com/package/'), location + assert response.status == 200, response.status + ctype = response.header("Content-Type") + assert 'application/rdf+xml' in ctype, ctype - def test_turtle(self): + def test_n3(self): url = url_for(controller='package', action='read', id='annakarenina') - response = self.app.get(url, headers={"Accept": "application/turtle"}) - assert response.status == 303, response.status - location = response.header("Location") - assert location.endswith(".ttl"), location - assert location.startswith('http://test.com/package/'), location + response = self.app.get(url, headers={"Accept": "text/n3"}) + assert response.status == 200, response.status + ctype = response.header("Content-Type") + assert 'text/n3' in ctype, ctype diff --git a/ckan/tests/functional/test_package.py b/ckan/tests/functional/test_package.py index d03979b3942..fe77095af5f 100644 --- a/ckan/tests/functional/test_package.py +++ b/ckan/tests/functional/test_package.py @@ -375,7 +375,7 @@ def test_read_plugin_hook(self): # existed before. I don't know if this is a problem? I expect it # can be fixed by allowing the package to be passed in to the plugin, # either via the function argument, or adding it to the c object. - assert plugin.calls['read'] == 2, plugin.calls + assert plugin.calls['read'] == 1, plugin.calls plugins.unload(plugin) def test_resource_list(self): diff --git a/ckan/tests/functional/test_storage.py b/ckan/tests/functional/test_storage.py index 1d6168b512c..27182b55e0c 100644 --- a/ckan/tests/functional/test_storage.py +++ b/ckan/tests/functional/test_storage.py @@ -18,6 +18,7 @@ def setup_class(cls): config.local_conf['ofs.impl'] = 'pairtree' config.local_conf['ckan.storage.bucket'] = 'ckantest' config.local_conf['ofs.storage_dir'] = '/tmp/ckan-test-ckanext-storage' + create_pairtree_marker( config.local_conf['ofs.storage_dir'] ) wsgiapp = make_app(config.global_conf, **config.local_conf) cls.app = paste.fixture.TestApp(wsgiapp) diff --git a/doc/linked-data-and-rdf.rst b/doc/linked-data-and-rdf.rst index 19442ef73f8..c236ba6496f 100644 --- a/doc/linked-data-and-rdf.rst +++ b/doc/linked-data-and-rdf.rst @@ -17,9 +17,7 @@ In CKAN >= 1.6.1, basic RDF support will be available directly in core. Configuration ------------- -.. todo:: fill this in (imagine that config instructions for extension will be - in extension but for >= 1.6.1 we have stuff in core) - +When using the built-in RDF support (CKAN >= 1.6.1) there is no configuration required. By default requests for RDF data will return the RDF generated from the built-in 'packages/read.rdf' template, which can be overridden using the extra-templates directive. Accessing Linked Data ===================== @@ -31,6 +29,11 @@ example:: curl -L -H "Accept: application/rdf+xml" http://thedatahub.org/dataset/gold-prices curl -L -H "Accept: text/n3" http://thedatahub.org/dataset/gold-prices +An alternative method of retrieving the data is to add .rdf to the name of the dataset to download:: + + curl -L http://thedatahub.org/dataset/gold-prices.rdf + curl -L http://thedatahub.org/dataset/gold-prices.n3 + Schema Mapping ==============