diff --git a/.travis.yml b/.travis.yml
index debf9925c02..c6503aca704 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,7 +5,8 @@ python:
env:
- PGVERSION=9.1
- PGVERSION=8.4
-script: ./bin/travis-build
+install: ./bin/travis-install-dependencies
+script: ./bin/travis-run-tests
notifications:
irc:
channels:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index b97798a4522..d0eeaa96d98 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -12,10 +12,30 @@ v2.2
API changes and deprecations:
+* The Solr schema file is now always named ``schema.xml`` regardless of the
+ CKAN version. Old schema files have been kept for backwards compatibility
+ but users are encouraged to point to the new unified one.
+* The `ckan.api_url` has been completely removed and it can no longer be used
* The edit() and after_update() methods of IPackageController plugins are now
called when updating a resource using the web frontend or the
resource_update API action [#1052]
+v2.1.1 2013-11-8
+================
+
+Bug fixes:
+ * Fix errors on preview on non-root locations (#960)
+ * Fix place-holder images on non-root locations (#1309)
+ * Don't accept invalid URLs in resource proxy (#1106)
+ * Make sure came_from url is local (#1039)
+ * Fix logout redirect in non-root locations (#1025)
+ * Wrong auth checks for sysadmins on package_create (#1184)
+ * Don't return private datasets on package_list (#1295)
+ * Stop tracking failing when no lang/encoding headers (#1192)
+ * Fix for paster db clean command getting frozen
+ * Fix organization not set when editing a dataset (#1199)
+ * Fix PDF previews (#1194)
+ * Fix preview failing on private datastore resources (#1221)
v2.1 2013-08-13
===============
@@ -96,6 +116,18 @@ Known issues:
* Under certain authorization setups the frontend for the groups functionality
may not work as expected (See #1176 #1175).
+v2.0.3 2013-11-8
+================
+
+Bug fixes:
+ * Fix errors on preview on non-root locations (#960)
+ * Don't accept invalid URLs in resource proxy (#1106)
+ * Make sure came_from url is local (#1039)
+ * Fix logout redirect in non-root locations (#1025)
+ * Don't return private datasets on package_list (#1295)
+ * Stop tracking failing when no lang/encoding headers (#1192)
+ * Fix for paster db clean command getting frozen
+
v2.0.2 2013-08-13
=================
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 7b15f8d5531..79aaa0214eb 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -23,7 +23,7 @@ contributions to CKAN.
----------------
-Reporting Issues
+Reporting issues
----------------
If you've found a bug in CKAN, open a new issue on CKAN's `GitHub Issues`_ (try
@@ -47,7 +47,7 @@ For contributing translations to CKAN, see
.. _coding standards:
----------------
-Coding Standards
+Coding standards
----------------
When writing code for CKAN, try to respect our coding standards:
@@ -61,6 +61,7 @@ When writing code for CKAN, try to respect our coding standards:
css-coding-standards
javascript-coding-standards
testing-coding-standards
+ upgrading-dependencies
* `CKAN coding standards `_
* `Python coding standards `_
@@ -68,10 +69,11 @@ When writing code for CKAN, try to respect our coding standards:
* `CSS coding standards `_
* `JavaScript coding standards `_
* `Testing coding standards `_
+* `Upgrading CKAN's dependencies `_
---------------
-Commit Messages
+Commit messages
---------------
Generally, follow the `commit guidelines from the Pro Git book`_:
@@ -104,7 +106,7 @@ Here's an example of a good CKAN commit message::
-------------------------------
-Frontend Development Guidelines
+Frontend development guidelines
-------------------------------
.. toctree::
@@ -126,7 +128,7 @@ Frontend Development Guidelines
---------------------
-Writing Documentation
+Writing documentation
---------------------
The quickest and easiest way to contribute documentation to CKAN is to sign up
@@ -149,7 +151,7 @@ the `documentation guidelines = 9.0, we don't need to use datastore's legacy mode.
-if [ $PGVERSION != '8.4' ]
-then
- psql -c 'CREATE USER datastore_default;' -U postgres
- sed -i -e 's/.*datastore.read_url.*/ckan.datastore.read_url = postgresql:\/\/datastore_default@\/datastore_test/' test-core.ini
- paster datastore set-permissions postgres -c test-core.ini
-else
- sed -i -e 's/.*datastore.read_url.*//' test-core.ini
-fi
-
-cat test-core.ini
-
-# Run mocha front-end tests
-# We need ckan to be running for some tests
-paster serve test-core.ini &
-sleep 5 # Make sure the server has fully started
-mocha-phantomjs http://localhost:5000/base/test/index.html
-# Did an error occur?
-MOCHA_ERROR=$?
-# We are done so kill ckan
-killall paster
-
-# And finally, run the nosetests
-nosetests --ckan --with-pylons=test-core.ini --nologcapture ckan ckanext --with-coverage --cover-package=ckanext --cover-package=ckan
-# Did an error occur?
-NOSE_ERROR=$?
-
-[ "0" -ne "$MOCHA_ERROR" ] && echo MOCKA tests have failed
-[ "0" -ne "$NOSE_ERROR" ] && echo NOSE tests have failed
-
-# If an error occurred in our tests make sure travis knows
-exit `expr $MOCHA_ERROR + $NOSE_ERROR`
diff --git a/bin/travis-install-dependencies b/bin/travis-install-dependencies
new file mode 100755
index 00000000000..e8cdeb89525
--- /dev/null
+++ b/bin/travis-install-dependencies
@@ -0,0 +1,53 @@
+#!/bin/bash
+
+# Exit immediately if any command fails
+set -e
+
+# Drop Travis' postgres cluster if we're building using a different pg version
+TRAVIS_PGVERSION='9.1'
+if [ $PGVERSION != $TRAVIS_PGVERSION ]
+then
+ sudo -u postgres pg_dropcluster --stop $TRAVIS_PGVERSION main
+ # Make psql use $PGVERSION
+ export PGCLUSTER=$PGVERSION/main
+fi
+
+# Install postgres and solr
+sudo apt-get update -qq
+sudo apt-get install postgresql-$PGVERSION solr-jetty libcommons-fileupload-java:amd64=1.2.2-1
+
+if [ $PGVERSION == '8.4' ]
+then
+ # force postgres to use 5432 as it's port
+ sudo sed -i -e 's/port = 5433/port = 5432/g' /etc/postgresql/8.4/main/postgresql.conf
+fi
+
+sudo service postgresql restart
+
+# Setup postgres' users and databases
+sudo -u postgres psql -c "CREATE USER ckan_default WITH PASSWORD 'pass';"
+sudo -u postgres psql -c "CREATE USER datastore_default WITH PASSWORD 'pass';"
+sudo -u postgres psql -c 'CREATE DATABASE ckan_test WITH OWNER ckan_default;'
+sudo -u postgres psql -c 'CREATE DATABASE datastore_test WITH OWNER ckan_default;'
+
+export PIP_USE_MIRRORS=true
+pip install -r requirements.txt
+pip install -r dev-requirements.txt
+
+python setup.py develop
+
+# Install npm dpes for mocha
+npm install -g mocha-phantomjs phantomjs
+
+paster db init -c test-core.ini
+
+# If Postgres >= 9.0, we don't need to use datastore's legacy mode.
+if [ $PGVERSION != '8.4' ]
+then
+ sed -i -e 's/.*datastore.read_url.*/ckan.datastore.read_url = postgresql:\/\/datastore_default:pass@\/datastore_test/' test-core.ini
+ paster datastore set-permissions postgres -c test-core.ini
+else
+ sed -i -e 's/.*datastore.read_url.*//' test-core.ini
+fi
+
+cat test-core.ini
diff --git a/bin/travis-run-tests b/bin/travis-run-tests
new file mode 100755
index 00000000000..5d60ea0646d
--- /dev/null
+++ b/bin/travis-run-tests
@@ -0,0 +1,27 @@
+#!/bin/sh
+
+# Configure Solr
+echo "NO_START=0\nJETTY_HOST=127.0.0.1\nJETTY_PORT=8983\nJAVA_HOME=$JAVA_HOME" | sudo tee /etc/default/jetty
+sudo cp ckan/config/solr/schema.xml /etc/solr/conf/schema.xml
+sudo service jetty restart
+
+# Run mocha front-end tests
+# We need ckan to be running for some tests
+paster serve test-core.ini &
+sleep 5 # Make sure the server has fully started
+mocha-phantomjs http://localhost:5000/base/test/index.html
+# Did an error occur?
+MOCHA_ERROR=$?
+# We are done so kill ckan
+killall paster
+
+# And finally, run the nosetests
+nosetests --ckan --reset-db --with-pylons=test-core.ini --nologcapture ckan ckanext
+# Did an error occur?
+NOSE_ERROR=$?
+
+[ "0" -ne "$MOCHA_ERROR" ] && echo MOCKA tests have failed
+[ "0" -ne "$NOSE_ERROR" ] && echo NOSE tests have failed
+
+# If an error occurred in our tests make sure travis knows
+exit `expr $MOCHA_ERROR + $NOSE_ERROR`
diff --git a/ckan/ckan_nose_plugin.py b/ckan/ckan_nose_plugin.py
index 2ec4d0988b9..877589a2204 100644
--- a/ckan/ckan_nose_plugin.py
+++ b/ckan/ckan_nose_plugin.py
@@ -19,21 +19,26 @@ def startContext(self, ctx):
if 'new_tests' in repr(ctx):
# We don't want to do the stuff below for new-style tests.
+ if not CkanNose.settings.reset_database:
+ model.repo.tables_created_and_initialised = True
return
if isclass(ctx):
if hasattr(ctx, "no_db") and ctx.no_db:
return
- if self.is_first_test or CkanNose.settings.ckan_migration:
+ if (not CkanNose.settings.reset_database
+ and not CkanNose.settings.ckan_migration):
+ model.Session.close_all()
+ model.repo.tables_created_and_initialised = True
+ model.repo.rebuild_db()
+ self.is_first_test = False
+ elif self.is_first_test or CkanNose.settings.ckan_migration:
model.Session.close_all()
model.repo.clean_db()
self.is_first_test = False
if CkanNose.settings.ckan_migration:
model.Session.close_all()
model.repo.upgrade_db()
- # init_db is run at the start of every class because
- # when you use an in-memory sqlite db, it appears that
- # the db is destroyed after every test when you Session.Remove().
## This is to make sure the configuration is run again.
## Plugins use configure to make their own tables and they
@@ -43,6 +48,9 @@ def startContext(self, ctx):
for plugin in PluginImplementations(IConfigurable):
plugin.configure(config)
+ # init_db is run at the start of every class because
+ # when you use an in-memory sqlite db, it appears that
+ # the db is destroyed after every test when you Session.Remove().
model.repo.init_db()
def options(self, parser, env):
@@ -66,6 +74,11 @@ def options(self, parser, env):
dest='segments',
help='A string containing a hex digits that represent which of'
'the 16 test segments to run. i.e 15af will run segments 1,5,a,f')
+ parser.add_option(
+ '--reset-db',
+ action='store_true',
+ dest='reset_database',
+ help='drop database and reinitialize before tests are run')
def wantClass(self, cls):
name = cls.__name__
diff --git a/ckan/config/deployment.ini_tmpl b/ckan/config/deployment.ini_tmpl
index a9e5c56c0e2..b3148cc6d68 100644
--- a/ckan/config/deployment.ini_tmpl
+++ b/ckan/config/deployment.ini_tmpl
@@ -66,8 +66,10 @@ ckan.auth.user_create_organizations = true
ckan.auth.user_delete_groups = true
ckan.auth.user_delete_organizations = true
ckan.auth.create_user_via_api = false
+ckan.auth.create_user_via_web = true
ckan.auth.roles_that_cascade_to_sub_groups = admin
+
## Search Settings
ckan.site_id = default
@@ -120,28 +122,9 @@ ckan.feeds.author_link =
## Storage Settings
-# Local file storage:
-#ofs.impl = pairtree
-#ofs.storage_dir = /var/lib/ckan/default
-
-# Google cloud storage:
-#ofs.impl = google
-#ofs.gs_access_key_id =
-#ofs.gs_secret_access_key =
-
-# S3 cloud storage:
-#ofs.impl = s3
-#ofs.aws_access_key_id = ....
-#ofs.aws_secret_access_key = ....
-
-# 'Bucket' to use for file storage
-#ckan.storage.bucket = default
-
-# Prefix for uploaded files (only used for pairtree)
-#ckan.storage.key_prefix = file/
-
-# The maximum content size, in bytes, for uploads
-#ckan.storage.max_content_length = 50000000
+#ckan.storage_path = /var/lib/ckan
+#ckan.max_resource_size = 10
+#ckan.max_image_size = 2
## Datapusher settings
diff --git a/ckan/config/middleware.py b/ckan/config/middleware.py
index 4df0aa84cb0..5ba02933d46 100644
--- a/ckan/config/middleware.py
+++ b/ckan/config/middleware.py
@@ -4,6 +4,7 @@
import logging
import json
import hashlib
+import os
import sqlalchemy as sa
from beaker.middleware import CacheMiddleware, SessionMiddleware
@@ -22,6 +23,7 @@
from ckan.plugins import PluginImplementations
from ckan.plugins.interfaces import IMiddleware
from ckan.lib.i18n import get_locales_from_config
+import ckan.lib.uploader as uploader
from ckan.config.environment import load_environment
import ckan.lib.app_globals as app_globals
@@ -147,6 +149,20 @@ def make_app(conf, full_stack=True, static_files=True, **app_conf):
cache_max_age=static_max_age)
static_parsers = [static_app, app]
+ storage_directory = uploader.get_storage_path()
+ if storage_directory:
+ path = os.path.join(storage_directory, 'storage')
+ try:
+ os.makedirs(path)
+ except OSError, e:
+ ## errno 17 is file already exists
+ if e.errno != 17:
+ raise
+
+ storage_app = StaticURLParser(path,
+ cache_max_age=static_max_age)
+ static_parsers.insert(0, storage_app)
+
# Configurable extra static file paths
extra_static_parsers = []
for public_path in config.get('extra_public_paths', '').split(','):
diff --git a/ckan/config/routing.py b/ckan/config/routing.py
index 016c65ae0e4..345ff070724 100644
--- a/ckan/config/routing.py
+++ b/ckan/config/routing.py
@@ -229,6 +229,7 @@ def make_map():
'history_ajax',
'follow',
'activity',
+ 'groups',
'unfollow',
'delete',
'api_data',
@@ -240,6 +241,8 @@ def make_map():
m.connect('dataset_activity', '/dataset/activity/{id}',
action='activity', ckan_icon='time')
m.connect('/dataset/activity/{id}/{offset}', action='activity')
+ m.connect('dataset_groups', '/dataset/groups/{id}',
+ action='groups', ckan_icon='group')
m.connect('/dataset/{id}.{format}', action='read')
m.connect('dataset_resources', '/dataset/resources/{id}',
action='resources', ckan_icon='reorder')
@@ -253,6 +256,8 @@ def make_map():
action='resource_edit', ckan_icon='edit')
m.connect('/dataset/{id}/resource/{resource_id}/download',
action='resource_download')
+ m.connect('/dataset/{id}/resource/{resource_id}/download/{filename}',
+ action='resource_download')
m.connect('/dataset/{id}/resource/{resource_id}/embed',
action='resource_embedded_dataviewer')
m.connect('/dataset/{id}/resource/{resource_id}/viewer',
diff --git a/ckan/config/solr/CHANGELOG.txt b/ckan/config/solr/CHANGELOG.txt
deleted file mode 100644
index 769c6b53735..00000000000
--- a/ckan/config/solr/CHANGELOG.txt
+++ /dev/null
@@ -1,29 +0,0 @@
-CKAN SOLR schemas changelog
-===========================
-
-v2.0 - (ckan>=2.0)
---------------------
-* Add _version_ field to make it compatible with solr 4.0
-* Remove stopwords
-* Add dataset_type field.
-* Add *_date autofield.
-
-v1.4 - (ckan>=1.7)
---------------------
-* Add Ascii folding filter to text fields.
-* Add capacity field for public, private access.
-* Add title_string so you can sort alphabetically on title.
-* Fields related to analytics, access and view counts.
-* Add data_dict field for the whole package_dict.
-* Add vocab_* dynamic field so it is possible to facet by vocabulary tags
-* Add copyField for text with source vocab_*
-
-v1.3 - (ckan>=1.5.1)
---------------------
-* Use the index_id (hash of dataset id + site_id) as uniqueKey (#1430)
-* Store extras (#1455)
-* Store dataset creation and modification date (#191)
-
-v1.2 - (ckan<=1.5)
---------------------
-* Original version
diff --git a/ckan/config/solr/README.txt b/ckan/config/solr/README.txt
index 9a3bccebfac..d8fcff40f42 100644
--- a/ckan/config/solr/README.txt
+++ b/ckan/config/solr/README.txt
@@ -1,30 +1,12 @@
-CKAN SOLR schemas
-=================
+CKAN Solr schema
+================
-This folder contains the latest and previous versions of the SOLR XML
-schema files used by CKAN. These can be use on the SOLR server to
-override the default SOLR schema. Please note that not all schemas are
-backwards compatible with old CKAN versions. Check the CHANGELOG.txt file
-in this same folder to check which version of the schema should you use
-depending on the CKAN version you are using.
+This folder contains the Solr schema file used by CKAN (schema.xml).
-Developers, when pushing changes to the SOLR schema:
+Starting from 2.2 this is the only file that should be used by users and
+modified by devs. The rest of files (schema-{version}.xml) are kept for
+backwards compatibility purposes and should not be used, as they might be
+removed in future versions.
-* Note that updates on the schema are only release based, i.e. all changes
- in the schema between releases will be part of the same new version of
- the schema.
-
-* Name the new version of the file using the following convention::
-
- schema-.xml
-
-* Update the `version` attribute of the `schema` tag in the new file::
-
-
-
-* Update the SUPPORTED_SCHEMA_VERSIONS list in `ckan/lib/search/__init__.py`
- Consider if the changes introduced are or are not compatible with
- previous schema versions.
-
-* Update the CHANGELOG.txt file with the new version, the CKAN version
- required and changes made to the schema.
+When upgrading CKAN, always check the CHANGELOG on each release to see if
+you need to update the schema file and reindex your datasets.
diff --git a/ckan/config/solr/schema-1.2.xml b/ckan/config/solr/schema-1.2.xml
index 2fb41dd96a1..4f9b11a580b 100644
--- a/ckan/config/solr/schema-1.2.xml
+++ b/ckan/config/solr/schema-1.2.xml
@@ -1,4 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+index_id
+text
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ckan/controllers/api.py b/ckan/controllers/api.py
index bcff15841a5..08ba3a8e641 100644
--- a/ckan/controllers/api.py
+++ b/ckan/controllers/api.py
@@ -839,7 +839,9 @@ def make_unicode(entity):
cls.log.debug('Retrieving request POST: %r' % request.POST)
cls.log.debug('Retrieving request GET: %r' % request.GET)
request_data = None
- if request.POST:
+ if request.POST and request.content_type == 'multipart/form-data':
+ request_data = dict(request.POST)
+ elif request.POST:
try:
keys = request.POST.keys()
# Parsing breaks if there is a = in the value, so for now
@@ -873,7 +875,7 @@ def make_unicode(entity):
raise ValueError(msg)
else:
request_data = {}
- if request_data:
+ if request_data and request.content_type != 'multipart/form-data':
try:
request_data = h.json.loads(request_data, encoding='utf8')
except ValueError, e:
diff --git a/ckan/controllers/group.py b/ckan/controllers/group.py
index 484457bde26..2c5e293a074 100644
--- a/ckan/controllers/group.py
+++ b/ckan/controllers/group.py
@@ -1,6 +1,8 @@
import re
+import os
import logging
import genshi
+import cgi
import datetime
from urllib import urlencode
@@ -425,6 +427,9 @@ def new(self, data=None, errors=None, error_summary=None):
return self._save_new(context, group_type)
data = data or {}
+ if not data.get('image_url', '').startswith('http'):
+ data.pop('image_url', None)
+
errors = errors or {}
error_summary = error_summary or {}
vars = {'data': data, 'errors': errors,
@@ -524,7 +529,6 @@ def _save_edit(self, id, context):
data_dict['id'] = id
context['allow_partial_update'] = True
group = self._action('group_update')(context, data_dict)
-
if id != group['name']:
self._force_reindex(group)
@@ -639,7 +643,7 @@ def member_new(self, id):
else:
c.user_role = 'member'
c.group_dict = self._action('group_show')(context, {'id': id})
- c.roles = self._action('member_roles_list')(context, {})
+ c.roles = self._action('member_roles_list')(context, {'group_type': 'group'})
except NotAuthorized:
abort(401, _('Unauthorized to add member to group %s') % '')
except NotFound:
@@ -756,11 +760,8 @@ def activity(self, id, offset=0):
context = {'model': model, 'session': model.Session,
'user': c.user or c.author, 'for_view': True}
- data_dict = {'id': id}
-
try:
- c.group_dict = get_action('group_show')(context, data_dict)
- c.group = context['group']
+ c.group_dict = self._get_group_dict(id)
except NotFound:
abort(404, _('Group not found'))
except NotAuthorized:
@@ -770,10 +771,9 @@ def activity(self, id, offset=0):
# Add the group's activity stream (already rendered to HTML) to the
# template context for the group/read.html template to retrieve later.
- c.group_activity_stream = get_action('group_activity_list_html')(
+ c.group_activity_stream = self._action('group_activity_list_html')(
context, {'id': c.group_dict['id'], 'offset': offset})
- #return render('group/activity_stream.html')
return render(self._activity_template(c.group_dict['type']))
def follow(self, id):
diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py
index b0adf5c5ac5..689b8e64355 100644
--- a/ckan/controllers/package.py
+++ b/ckan/controllers/package.py
@@ -1,11 +1,15 @@
import logging
from urllib import urlencode
import datetime
+import os
+import mimetypes
+import cgi
from pylons import config
from genshi.template import MarkupTemplate
from genshi.template.text import NewTextTemplate
from paste.deploy.converters import asbool
+import paste.fileapp
import ckan.logic as logic
import ckan.lib.base as base
@@ -18,6 +22,7 @@
import ckan.model as model
import ckan.lib.datapreview as datapreview
import ckan.lib.plugins
+import ckan.lib.uploader as uploader
import ckan.plugins as p
import ckan.lib.render
@@ -221,7 +226,7 @@ def pager_url(q=None, page=None):
'groups': _('Groups'),
'tags': _('Tags'),
'res_format': _('Formats'),
- 'license_id': _('License'),
+ 'license_id': _('Licenses'),
}
for facet in g.facets:
@@ -310,7 +315,7 @@ def resources(self, id):
data_dict = {'id': id}
try:
- check_access('package_update', context)
+ check_access('package_update', context, data_dict)
except NotAuthorized, e:
abort(401, _('User %r not authorized to edit %s') % (c.user, id))
# check if package exists
@@ -550,7 +555,7 @@ def resource_edit(self, id, resource_id, data=None, errors=None,
del data['save']
context = {'model': model, 'session': model.Session,
- 'api_version': 3,
+ 'api_version': 3, 'for_edit': True,
'user': c.user or c.author, 'auth_user_obj': c.userobj}
data['package_id'] = id
@@ -571,7 +576,7 @@ def resource_edit(self, id, resource_id, data=None, errors=None,
id=id, resource_id=resource_id))
context = {'model': model, 'session': model.Session,
- 'api_version': 3,
+ 'api_version': 3, 'for_edit': True,
'user': c.user or c.author, 'auth_user_obj': c.userobj}
pkg_dict = get_action('package_show')(context, {'id': id})
if pkg_dict['state'].startswith('draft'):
@@ -621,7 +626,8 @@ def new_resource(self, id, data=None, errors=None, error_summary=None):
# see if we have any data that we are trying to save
data_provided = False
for key, value in data.iteritems():
- if value and key != 'resource_type':
+ if ((value or isinstance(value, cgi.FieldStorage))
+ and key != 'resource_type'):
data_provided = True
break
@@ -1204,10 +1210,10 @@ def _resource_preview(self, data_dict):
or datapreview.get_preview_plugin(
data_dict, return_first=True))
- def resource_download(self, id, resource_id):
+ def resource_download(self, id, resource_id, filename=None):
"""
- Provides a direct download by redirecting the user to the url stored
- against this resource.
+ Provides a direct download by either redirecting the user to the url stored
+ or downloading an uploaded file directly.
"""
context = {'model': model, 'session': model.Session,
'user': c.user or c.author, 'auth_user_obj': c.userobj}
@@ -1220,7 +1226,20 @@ def resource_download(self, id, resource_id):
except NotAuthorized:
abort(401, _('Unauthorized to read resource %s') % id)
- if not 'url' in rsc:
+ if rsc.get('url_type') == 'upload':
+ upload = uploader.ResourceUpload(rsc)
+ filepath = upload.get_path(rsc['id'])
+ fileapp = paste.fileapp.FileApp(filepath)
+ try:
+ status, headers, app_iter = request.call_application(fileapp)
+ except OSError:
+ abort(404, _('Resource data not found'))
+ response.headers.update(dict(headers))
+ content_type, content_enc = mimetypes.guess_type(rsc.get('url',''))
+ response.headers['Content-Type'] = content_type
+ response.status = status
+ return app_iter
+ elif not 'url' in rsc:
abort(404, _('No download is available'))
redirect(rsc['url'])
@@ -1283,6 +1302,62 @@ def followers(self, id=None):
return render('package/followers.html')
+ def groups(self, id):
+ context = {'model': model, 'session': model.Session,
+ 'user': c.user or c.author, 'for_view': True,
+ 'auth_user_obj': c.userobj, 'use_cache': False}
+ data_dict = {'id': id}
+ try:
+ c.pkg_dict = get_action('package_show')(context, data_dict)
+ except NotFound:
+ abort(404, _('Dataset not found'))
+ except NotAuthorized:
+ abort(401, _('Unauthorized to read dataset %s') % id)
+
+ if request.method == 'POST':
+ new_group = request.POST.get('group_added')
+ if new_group:
+ data_dict = {"id": new_group,
+ "object": id,
+ "object_type": 'package',
+ "capacity": 'public'}
+ try:
+ get_action('member_create')(context, data_dict)
+ except NotFound:
+ abort(404, _('Group not found'))
+
+ removed_group = request.POST.get('group_removed')
+ if removed_group:
+ data_dict = {"id": removed_group,
+ "object": id,
+ "object_type": 'package'}
+
+ try:
+ get_action('member_delete')(context, data_dict)
+ except NotFound:
+ abort(404, _('Group not found'))
+ redirect(h.url_for(controller='package',
+ action='groups', id=id))
+
+
+
+ context['is_member'] = True
+ users_groups = get_action('group_list_authz')(context, data_dict)
+
+ pkg_group_ids = set(group['id'] for group
+ in c.pkg_dict.get('groups', []))
+ user_group_ids = set(group['id'] for group
+ in users_groups)
+
+ c.group_dropdown = [[group['id'], group['display_name']]
+ for group in users_groups if
+ group['id'] not in pkg_group_ids]
+
+ for group in c.pkg_dict.get('groups', []):
+ group['user_member'] = (group['id'] in user_group_ids)
+
+ return render('package/group_list.html')
+
def activity(self, id):
'''Render this package's public activity stream page.'''
diff --git a/ckan/controllers/user.py b/ckan/controllers/user.py
index 1c6fefff344..4f30bb4ed91 100644
--- a/ckan/controllers/user.py
+++ b/ckan/controllers/user.py
@@ -68,7 +68,7 @@ def _setup_template_variables(self, context, data_dict):
try:
user_dict = get_action('user_show')(context, data_dict)
except NotFound:
- h.redirect_to(controller='user', action='login', id=None)
+ abort(404, _('User not found'))
except NotAuthorized:
abort(401, _('Not authorized to see this page'))
c.user_dict = user_dict
@@ -117,10 +117,6 @@ def read(self, id=None):
'for_view': True}
data_dict = {'id': id,
'user_obj': c.userobj}
- try:
- check_access('user_show', context, data_dict)
- except NotAuthorized:
- abort(401, _('Not authorized to see this page'))
context['with_related'] = True
@@ -455,7 +451,7 @@ def perform_reset(self, id):
# reuse of the url
context = {'model': model, 'session': model.Session,
'user': id,
- 'keep_sensitive_data': True}
+ 'keep_email': True}
try:
check_access('user_reset', context)
@@ -466,10 +462,6 @@ def perform_reset(self, id):
data_dict = {'id': id}
user_dict = get_action('user_show')(context, data_dict)
- # Be a little paranoid, and get rid of sensitive data that's
- # not needed.
- user_dict.pop('apikey', None)
- user_dict.pop('reset_key', None)
user_obj = context['user_obj']
except NotFound, e:
abort(404, _('User not found'))
diff --git a/ckan/lib/app_globals.py b/ckan/lib/app_globals.py
index 38c777f7555..38138e87815 100644
--- a/ckan/lib/app_globals.py
+++ b/ckan/lib/app_globals.py
@@ -44,7 +44,6 @@
'ckan.template_footer_end': {},
'ckan.dumps_url': {},
'ckan.dumps_format': {},
- 'ckan.api_url': {},
'ofs.impl': {'name': 'ofs_impl'},
'ckan.homepage_style': {'default': '1'},
diff --git a/ckan/lib/cli.py b/ckan/lib/cli.py
index 80f1615d675..0b0ebd84287 100644
--- a/ckan/lib/cli.py
+++ b/ckan/lib/cli.py
@@ -10,6 +10,8 @@
import ckan.include.rcssmin as rcssmin
import ckan.lib.fanstatic_resources as fanstatic_resources
import sqlalchemy as sa
+import urlparse
+import routes
import paste.script
from paste.registry import Registry
@@ -99,6 +101,12 @@ def _load_config(self):
self.translator_obj = MockTranslator()
self.registry.register(pylons.translator, self.translator_obj)
+ ## give routes enough information to run url_for
+ parsed = urlparse.urlparse(conf.get('ckan.site_url', 'http://0.0.0.0'))
+ request_config = routes.request_config()
+ request_config.host = parsed.netloc + parsed.path
+ request_config.protocol = parsed.scheme
+
def _setup_app(self):
cmd = paste.script.appinstall.SetupCommand('setup-app')
cmd.run([self.filename])
@@ -122,6 +130,7 @@ class ManageDb(CkanCommand):
db load-only FILE_PATH - load a pg_dump from a file but don\'t do
the schema upgrade or search indexing
db create-from-model - create database from the model (indexes not made)
+ db migrate-filestore - migrate all uploaded data from the 2.1 filesore.
'''
summary = __doc__.split('\n')[0]
usage = __doc__
@@ -179,6 +188,8 @@ def command(self):
print 'Creating DB: SUCCESS'
elif cmd == 'send-rdf':
self.send_rdf()
+ elif cmd == 'migrate-filestore':
+ self.migrate_filestore()
else:
print 'Command %s not recognized' % cmd
sys.exit(1)
@@ -311,6 +322,45 @@ def send_rdf(self):
talis = ckan.lib.talis.Talis()
return talis.send_rdf(talis_store, username, password)
+ def migrate_filestore(self):
+ from ckan.model import Session
+ import requests
+ from ckan.lib.uploader import ResourceUpload
+ results = Session.execute("select id, revision_id, url from resource "
+ "where resource_type = 'file.upload' "
+ "and (url_type <> 'upload' or url_type is null)"
+ "and url like '%storage%'")
+ for id, revision_id, url in results:
+ response = requests.get(url, stream=True)
+ if response.status_code != 200:
+ print "failed to fetch %s (code %s)" % (url,
+ response.status_code)
+ continue
+ resource_upload = ResourceUpload({'id': id})
+ assert resource_upload.storage_path, "no storage configured aborting"
+
+ directory = resource_upload.get_directory(id)
+ filepath = resource_upload.get_path(id)
+ try:
+ os.makedirs(directory)
+ except OSError, e:
+ ## errno 17 is file already exists
+ if e.errno != 17:
+ raise
+
+ with open(filepath, 'wb+') as out:
+ for chunk in response.iter_content(1024):
+ if chunk:
+ out.write(chunk)
+
+ Session.execute("update resource set url_type = 'upload'"
+ "where id = '%s'" % id)
+ Session.execute("update resource_revision set url_type = 'upload'"
+ "where id = '%s' and "
+ "revision_id = '%s'" % (id, revision_id))
+ Session.commit()
+ print "Saved url %s" % url
+
def version(self):
from ckan.model import Session
print Session.execute('select version from migrate_version;').fetchall()
diff --git a/ckan/lib/dictization/model_dictize.py b/ckan/lib/dictization/model_dictize.py
index 34fb738b950..ede1fd85c7f 100644
--- a/ckan/lib/dictization/model_dictize.py
+++ b/ckan/lib/dictization/model_dictize.py
@@ -10,22 +10,23 @@
import ckan.lib.dictization as d
import ckan.new_authz as new_authz
import ckan.lib.search as search
+import ckan.lib.munge as munge
## package save
def group_list_dictize(obj_list, context,
- sort_key=lambda x:x['display_name'], reverse=False):
+ sort_key=lambda x:x['display_name'], reverse=False,
+ with_package_counts=True):
active = context.get('active', True)
with_private = context.get('include_private_packages', False)
- query = search.PackageSearchQuery()
-
- q = {'q': '+capacity:public' if not with_private else '*:*',
- 'fl': 'groups', 'facet.field': ['groups', 'owner_org'],
- 'facet.limit': -1, 'rows': 1}
-
- query.run(q)
+ if with_package_counts:
+ query = search.PackageSearchQuery()
+ q = {'q': '+capacity:public' if not with_private else '*:*',
+ 'fl': 'groups', 'facet.field': ['groups', 'owner_org'],
+ 'facet.limit': -1, 'rows': 1}
+ query.run(q)
result_list = []
@@ -39,12 +40,26 @@ def group_list_dictize(obj_list, context,
if active and obj.state not in ('active', 'pending'):
continue
- group_dict['display_name'] = obj.display_name
-
- if obj.is_organization:
- group_dict['packages'] = query.facets['owner_org'].get(obj.id, 0)
- else:
- group_dict['packages'] = query.facets['groups'].get(obj.name, 0)
+ group_dict['display_name'] = (group_dict.get('title') or
+ group_dict.get('name'))
+
+ image_url = group_dict.get('image_url')
+ group_dict['image_display_url'] = image_url
+ if image_url and not image_url.startswith('http'):
+ #munge here should not have an effect only doing it incase
+ #of potential vulnerability of dodgy api input
+ image_url = munge.munge_filename(image_url)
+ group_dict['image_display_url'] = h.url_for_static(
+ 'uploads/group/%s' % group_dict.get('image_url'),
+ qualified=True
+ )
+
+ if with_package_counts:
+ facets = query.facets
+ if obj.is_organization:
+ group_dict['packages'] = facets['owner_org'].get(obj.id, 0)
+ else:
+ group_dict['packages'] = facets['groups'].get(obj.name, 0)
if context.get('for_view'):
if group_dict['is_organization']:
@@ -128,14 +143,29 @@ def _unified_resource_format(format_):
return format_new
def resource_dictize(res, context):
+ model = context['model']
resource = d.table_dictize(res, context)
+ resource_group_id = resource['resource_group_id']
extras = resource.pop("extras", None)
if extras:
resource.update(extras)
resource['format'] = _unified_resource_format(res.format)
# some urls do not have the protocol this adds http:// to these
url = resource['url']
- if not urlparse.urlsplit(url).scheme:
+ ## for_edit is only called at the times when the dataset is to be edited
+ ## in the frontend. Without for_edit the whole qualified url is returned.
+ if resource.get('url_type') == 'upload' and not context.get('for_edit'):
+ resource_group = model.Session.query(
+ model.ResourceGroup).get(resource_group_id)
+ last_part = url.split('/')[-1]
+ cleaned_name = munge.munge_filename(last_part)
+ resource['url'] = h.url_for(controller='package',
+ action='resource_download',
+ id=resource_group.package_id,
+ resource_id=res.id,
+ filename=cleaned_name,
+ qualified=True)
+ elif not urlparse.urlsplit(url).scheme and not context.get('for_edit'):
resource['url'] = u'http://' + url.lstrip('/')
return resource
@@ -248,7 +278,11 @@ def package_dictize(pkg, context):
.where(member_rev.c.state == 'active') \
.where(group.c.is_organization == False)
result = _execute_with_revision(q, member_rev, context)
- result_dict["groups"] = d.obj_list_dictize(result, context)
+ context['with_capacity'] = False
+ ## no package counts as cannot fetch from search index at the same
+ ## time as indexing to it.
+ result_dict["groups"] = group_list_dictize(result, context,
+ with_package_counts=False)
#owning organization
group_rev = model.group_revision_table
q = select([group_rev]
@@ -357,6 +391,16 @@ def group_dictize(group, context):
for item in plugins.PluginImplementations(plugin):
result_dict = item.before_view(result_dict)
+ image_url = result_dict.get('image_url')
+ result_dict['image_display_url'] = image_url
+ if image_url and not image_url.startswith('http'):
+ #munge here should not have an effect only doing it incase
+ #of potential vulnerability of dodgy api input
+ image_url = munge.munge_filename(image_url)
+ result_dict['image_display_url'] = h.url_for_static(
+ 'uploads/group/%s' % result_dict.get('image_url'),
+ qualified = True
+ )
return result_dict
def tag_list_dictize(tag_list, context):
@@ -431,6 +475,9 @@ def user_list_dictize(obj_list, context,
for obj in obj_list:
user_dict = user_dictize(obj, context)
+ user_dict.pop('reset_key', None)
+ user_dict.pop('apikey', None)
+ user_dict.pop('email', None)
result_list.append(user_dict)
return sorted(result_list, key=sort_key, reverse=reverse)
@@ -455,13 +502,24 @@ def user_dictize(user, context):
requester = context.get('user')
- if not (new_authz.is_sysadmin(requester) or
- requester == user.name or
- context.get('keep_sensitive_data', False)):
- # If not sysadmin or the same user, strip sensible info
- result_dict.pop('apikey', None)
- result_dict.pop('reset_key', None)
- result_dict.pop('email', None)
+ reset_key = result_dict.pop('reset_key', None)
+ apikey = result_dict.pop('apikey', None)
+ email = result_dict.pop('email', None)
+
+ if context.get('keep_email', False):
+ result_dict['email'] = email
+
+ if context.get('keep_apikey', False):
+ result_dict['apikey'] = email
+
+ if requester == user.name:
+ result_dict['apikey'] = apikey
+ result_dict['email'] = email
+
+ ## this should not really really be needed but tests r it
+ if new_authz.is_sysadmin(requester):
+ result_dict['apikey'] = apikey
+ result_dict['email'] = email
model = context['model']
session = model.Session
@@ -632,3 +690,20 @@ def user_following_dataset_dictize(follower, context):
def user_following_group_dictize(follower, context):
return d.table_dictize(follower, context)
+
+ base_columns = set(['id', 'resource_id', 'title', 'description',
+ 'view_type', 'order', 'config'])
+
+def resource_view_dictize(resource_view, context):
+ dictized = d.table_dictize(resource_view, context)
+ dictized.pop('order')
+ config = dictized.pop('config', {})
+ dictized.update(config)
+ return dictized
+
+def resource_view_list_dictize(resource_views, context):
+ resource_view_dicts = []
+ for view in resource_views:
+ resource_view_dicts.append(resource_view_dictize(view, context))
+ return resource_view_dicts
+
diff --git a/ckan/lib/dictization/model_save.py b/ckan/lib/dictization/model_save.py
index 88bc106bac9..56ba6ec0758 100644
--- a/ckan/lib/dictization/model_save.py
+++ b/ckan/lib/dictization/model_save.py
@@ -41,8 +41,8 @@ def resource_dict_save(res_dict, context):
# this is an internal field so ignore
# FIXME This helps get the tests to pass but is a hack and should
# be fixed properly. basically don't update the format if not needed
- if (key == 'format' and value == obj.format
- or value == d.model_dictize._unified_resource_format(obj.format)):
+ if (key == 'format' and (value == obj.format
+ or value == d.model_dictize._unified_resource_format(obj.format))):
continue
setattr(obj, key, value)
else:
diff --git a/ckan/lib/email_notifications.py b/ckan/lib/email_notifications.py
index ba245872531..7f24b978285 100644
--- a/ckan/lib/email_notifications.py
+++ b/ckan/lib/email_notifications.py
@@ -221,7 +221,7 @@ def get_and_send_notifications_for_user(user):
def get_and_send_notifications_for_all_users():
context = {'model': model, 'session': model.Session, 'ignore_auth': True,
- 'keep_sensitive_data': True}
+ 'keep_email': True}
users = logic.get_action('user_list')(context, {})
for user in users:
get_and_send_notifications_for_user(user)
diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py
index 9a2b83ddfb1..f5f0a7e197f 100644
--- a/ckan/lib/helpers.py
+++ b/ckan/lib/helpers.py
@@ -37,6 +37,7 @@
import ckan.lib.maintain as maintain
import ckan.lib.datapreview as datapreview
import ckan.logic as logic
+import ckan.lib.uploader as uploader
from ckan.common import (
_, ungettext, g, c, request, session, json, OrderedDict
@@ -1664,6 +1665,10 @@ def new_activities():
action = logic.get_action('dashboard_new_activities_count')
return action({}, {})
+def uploads_enabled():
+ if uploader.get_storage_path():
+ return True
+ return False
def get_featured_organizations(count=1):
'''Returns a list of favourite organization in the form
@@ -1836,6 +1841,7 @@ def get_site_statistics():
'radio',
'submit',
'asbool',
+ 'uploads_enabled',
'get_featured_organizations',
'get_featured_groups',
'get_site_statistics',
diff --git a/ckan/lib/munge.py b/ckan/lib/munge.py
index c943e1b113f..a47c8c02258 100644
--- a/ckan/lib/munge.py
+++ b/ckan/lib/munge.py
@@ -105,6 +105,13 @@ def munge_tag(tag):
tag = _munge_to_length(tag, model.MIN_TAG_LENGTH, model.MAX_TAG_LENGTH)
return tag
+def munge_filename(filename):
+ filename = substitute_ascii_equivalents(filename)
+ filename = filename.lower().strip()
+ filename = re.sub(r'[^a-zA-Z0-9. ]', '', filename).replace(' ', '-')
+ filename = _munge_to_length(filename, 3, 100)
+ return filename
+
def _munge_to_length(string, min_length, max_length):
'''Pad/truncates a string'''
if len(string) < min_length:
diff --git a/ckan/lib/uploader.py b/ckan/lib/uploader.py
new file mode 100644
index 00000000000..a9c843a0d0d
--- /dev/null
+++ b/ckan/lib/uploader.py
@@ -0,0 +1,225 @@
+import os
+import cgi
+import pylons
+import datetime
+import ckan.lib.munge as munge
+import logging
+import ckan.logic as logic
+
+
+config = pylons.config
+log = logging.getLogger(__name__)
+
+_storage_path = None
+_max_resource_size = None
+_max_image_size = None
+
+
+def get_storage_path():
+ '''Function to cache storage path'''
+ global _storage_path
+
+ #None means it has not been set. False means not in config.
+ if _storage_path is None:
+ storage_path = config.get('ckan.storage_path')
+ ofs_impl = config.get('ofs.impl')
+ ofs_storage_dir = config.get('ofs.storage_dir')
+ if storage_path:
+ _storage_path = storage_path
+ elif ofs_impl == 'pairtree' and ofs_storage_dir:
+ log.warn('''Please use config option ckan.storage_path instaed of
+ ofs.storage_path''')
+ _storage_path = ofs_storage_dir
+ return _storage_path
+ elif ofs_impl:
+ log.critical('''We only support local file storage form version 2.2
+ of ckan please specify ckan.storage_path in your
+ config for your uploads''')
+ _storage_path = False
+ else:
+ log.critical('''Please specify a ckan.storage_path in your config
+ for your uploads''')
+ _storage_path = False
+
+ return _storage_path
+
+
+def get_max_image_size():
+ global _max_image_size
+ if _max_image_size is None:
+ _max_image_size = int(config.get('ckan.max_image_size', 2))
+ return _max_image_size
+
+
+def get_max_resource_size():
+ global _max_resource_size
+ if _max_resource_size is None:
+ _max_resource_size = int(config.get('ckan.max_resource_size', 10))
+ return _max_resource_size
+
+
+class Upload(object):
+ def __init__(self, object_type, old_filename=None):
+ ''' Setup upload by creating a subdirectory of the storage directory
+ of name object_type. old_filename is the name of the file in the url
+ field last time'''
+
+ self.storage_path = None
+ self.filename = None
+ self.filepath = None
+ path = get_storage_path()
+ if not path:
+ return
+ self.storage_path = os.path.join(path, 'storage',
+ 'uploads', object_type)
+ try:
+ os.makedirs(self.storage_path)
+ except OSError, e:
+ ## errno 17 is file already exists
+ if e.errno != 17:
+ raise
+ self.object_type = object_type
+ self.old_filename = old_filename
+ if old_filename:
+ self.old_filepath = os.path.join(self.storage_path, old_filename)
+
+ def update_data_dict(self, data_dict, url_field, file_field, clear_field):
+ ''' Manipulate data from the data_dict. url_field is the name of the
+ field where the upload is going to be. file_field is name of the key
+ where the FieldStorage is kept (i.e the field where the file data
+ actually is). clear_field is the name of a boolean field which
+ requests the upload to be deleted. This needs to be called before
+ it reaches any validators'''
+
+ self.url = data_dict.get(url_field, '')
+ self.clear = data_dict.pop(clear_field, None)
+ self.file_field = file_field
+ self.upload_field_storage = data_dict.pop(file_field, None)
+
+ if not self.storage_path:
+ return
+
+ if isinstance(self.upload_field_storage, cgi.FieldStorage):
+ self.filename = self.upload_field_storage.filename
+ self.filename = str(datetime.datetime.utcnow()) + self.filename
+ self.filename = munge.munge_filename(self.filename)
+ self.filepath = os.path.join(self.storage_path, self.filename)
+ data_dict[url_field] = self.filename
+ self.upload_file = self.upload_field_storage.file
+ self.tmp_filepath = self.filepath + '~'
+ ### keep the file if there has been no change
+ elif self.old_filename and not self.old_filename.startswith('http'):
+ if not self.clear:
+ data_dict[url_field] = self.old_filename
+ if self.clear and self.url == self.old_filename:
+ data_dict[url_field] = ''
+
+ def upload(self, max_size=2):
+ ''' Actually upload the file.
+ This should happen just before a commit but after the data has
+ been validated and flushed to the db. This is so we do not store
+ anything unless the request is actually good.
+ max_size is size in MB maximum of the file'''
+
+ if self.filename:
+ output_file = open(self.tmp_filepath, 'wb')
+ self.upload_file.seek(0)
+ current_size = 0
+ while True:
+ current_size = current_size + 1
+ # MB chuncks
+ data = self.upload_file.read(2 ** 20)
+ if not data:
+ break
+ output_file.write(data)
+ if current_size > max_size:
+ os.remove(self.tmp_filepath)
+ raise logic.ValidationError(
+ {self.file_field: ['File upload too large']}
+ )
+ output_file.close()
+ os.rename(self.tmp_filepath, self.filepath)
+ self.clear = True
+
+ if (self.clear and self.old_filename
+ and not self.old_filename.startswith('http')):
+ try:
+ os.remove(self.old_filepath)
+ except OSError, e:
+ pass
+
+
+class ResourceUpload(object):
+ def __init__(self, resource):
+ path = get_storage_path()
+ if not path:
+ self.storage_path = None
+ return
+ self.storage_path = os.path.join(path, 'resources')
+ try:
+ os.makedirs(self.storage_path)
+ except OSError, e:
+ ## errno 17 is file already exists
+ if e.errno != 17:
+ raise
+ self.filename = None
+
+ url = resource.get('url')
+ upload_field_storage = resource.pop('upload', None)
+ self.clear = resource.pop('clear_upload', None)
+
+ if isinstance(upload_field_storage, cgi.FieldStorage):
+ self.filename = upload_field_storage.filename
+ self.filename = munge.munge_filename(self.filename)
+ resource['url'] = self.filename
+ resource['url_type'] = 'upload'
+ self.upload_file = upload_field_storage.file
+ elif self.clear:
+ resource['url_type'] = ''
+
+ def get_directory(self, id):
+ directory = os.path.join(self.storage_path,
+ id[0:3], id[3:6])
+ return directory
+
+ def get_path(self, id):
+ directory = self.get_directory(id)
+ filepath = os.path.join(directory, id[6:])
+ return filepath
+
+ def upload(self, id, max_size=10):
+ if not self.storage_path:
+ return
+ directory = self.get_directory(id)
+ filepath = self.get_path(id)
+ if self.filename:
+ try:
+ os.makedirs(directory)
+ except OSError, e:
+ ## errno 17 is file already exists
+ if e.errno != 17:
+ raise
+ tmp_filepath = filepath + '~'
+ output_file = open(tmp_filepath, 'wb+')
+ self.upload_file.seek(0)
+ current_size = 0
+ while True:
+ current_size = current_size + 1
+ #MB chunks
+ data = self.upload_file.read(2 ** 20)
+ if not data:
+ break
+ output_file.write(data)
+ if current_size > max_size:
+ os.remove(tmp_filepath)
+ raise logic.ValidationError(
+ {'upload': ['File upload too large']}
+ )
+ output_file.close()
+ os.rename(tmp_filepath, filepath)
+
+ if self.clear:
+ try:
+ os.remove(filepath)
+ except OSError, e:
+ pass
diff --git a/ckan/logic/__init__.py b/ckan/logic/__init__.py
index 4a474e26432..c3d04665370 100644
--- a/ckan/logic/__init__.py
+++ b/ckan/logic/__init__.py
@@ -388,7 +388,7 @@ def get_action(action):
resolved_action_plugins[name]
)
)
- log.debug('Auth function %r was inserted', plugin.name)
+ log.debug('Action function {0} from plugin {1} was inserted'.format(name, plugin.name))
resolved_action_plugins[name] = plugin.name
# Extensions are exempted from the auth audit for now
# This needs to be resolved later
diff --git a/ckan/logic/action/create.py b/ckan/logic/action/create.py
index ce321b944e3..83500a28abc 100644
--- a/ckan/logic/action/create.py
+++ b/ckan/logic/action/create.py
@@ -17,6 +17,7 @@
import ckan.lib.dictization.model_dictize as model_dictize
import ckan.lib.dictization.model_save as model_save
import ckan.lib.navl.dictization_functions
+import ckan.lib.uploader as uploader
import ckan.lib.navl.validators as validators
import ckan.lib.mailer as mailer
@@ -90,7 +91,7 @@ def package_create(context, data_dict):
:param extras: the dataset's extras (optional), extras are arbitrary
(key: value) metadata items that can be added to datasets, each extra
dictionary should have keys ``'key'`` (a string), ``'value'`` (a
- string), and optionally ``'deleted'``
+ string)
:type extras: list of dataset extra dictionaries
:param relationships_as_object: see ``package_relationship_create()`` for
the format of relationship dictionaries (optional)
@@ -238,6 +239,8 @@ def resource_create(context, data_dict):
:type cache_last_updated: iso date string
:param webstore_last_updated: (optional)
:type webstore_last_updated: iso date string
+ :param upload: (optional)
+ :type upload: FieldStorage (optional) needs multipart/form-data
:returns: the newly created resource
:rtype: dictionary
@@ -255,15 +258,31 @@ def resource_create(context, data_dict):
if not 'resources' in pkg_dict:
pkg_dict['resources'] = []
+
+ upload = uploader.ResourceUpload(data_dict)
+
pkg_dict['resources'].append(data_dict)
try:
- pkg_dict = _get_action('package_update')(context, pkg_dict)
+ context['defer_commit'] = True
+ context['use_cache'] = False
+ _get_action('package_update')(context, pkg_dict)
+ context.pop('defer_commit')
except ValidationError, e:
errors = e.error_dict['resources'][-1]
raise ValidationError(errors)
- return pkg_dict['resources'][-1]
+ ## Get out resource_id resource from model as it will not appear in
+ ## package_show until after commit
+ upload.upload(context['package'].resources[-1].id,
+ uploader.get_max_resource_size())
+ model.repo.commit()
+
+ ## Run package show again to get out actual last_resource
+ pkg_dict = _get_action('package_show')(context, {'id': package_id})
+ resource = pkg_dict['resources'][-1]
+
+ return resource
def related_create(context, data_dict):
@@ -450,8 +469,7 @@ def member_create(context, data_dict=None):
if not obj:
raise NotFound('%s was not found.' % obj_type.title())
- # User must be able to update the group to add a member to it
- _check_access('group_update', context, data_dict)
+ _check_access('member_create', context, data_dict)
# Look up existing, in case it exists
member = model.Session.query(model.Member).\
@@ -478,7 +496,9 @@ def _group_or_org_create(context, data_dict, is_org=False):
session = context['session']
data_dict['is_organization'] = is_org
-
+ upload = uploader.Upload('group')
+ upload.update_data_dict(data_dict, 'image_url',
+ 'image_upload', 'clear_upload')
# get the schema
group_plugin = lib_plugins.lookup_group_plugin(
group_type=data_dict.get('type'))
@@ -556,6 +576,7 @@ def _group_or_org_create(context, data_dict, is_org=False):
logic.get_action('activity_create')(activity_create_context,
activity_dict)
+ upload.upload(uploader.get_max_image_size())
if not context.get('defer_commit'):
model.repo.commit()
context["group"] = group
@@ -821,7 +842,8 @@ def user_create(context, data_dict):
#
# The context is copied so as not to clobber the caller's context dict.
user_dictize_context = context.copy()
- user_dictize_context['keep_sensitive_data'] = True
+ user_dictize_context['keep_apikey'] = True
+ user_dictize_context['keep_email'] = True
user_dict = model_dictize.user_dictize(user, user_dictize_context)
context['user_obj'] = user
diff --git a/ckan/logic/action/delete.py b/ckan/logic/action/delete.py
index 0ff4f058428..2fdaf2f6563 100644
--- a/ckan/logic/action/delete.py
+++ b/ckan/logic/action/delete.py
@@ -228,8 +228,7 @@ def member_delete(context, data_dict=None):
if not obj:
raise NotFound('%s was not found.' % obj_type.title())
- # User must be able to update the group to remove a member from it
- _check_access('group_update', context, data_dict)
+ _check_access('member_create', context, data_dict)
member = model.Session.query(model.Member).\
filter(model.Member.table_name == obj_type).\
diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py
index ca46ac2ff57..502b1bf92c9 100644
--- a/ckan/logic/action/get.py
+++ b/ckan/logic/action/get.py
@@ -335,6 +335,7 @@ def translated_capacity(capacity):
def _group_or_org_list(context, data_dict, is_org=False):
model = context['model']
+ user = context['user']
api = context.get('api_version')
groups = data_dict.get('groups')
ref_group_by = 'id' if api == 2 else 'name'
@@ -464,7 +465,7 @@ def group_list_authz(context, data_dict):
_check_access('group_list_authz',context, data_dict)
sysadmin = new_authz.is_sysadmin(user)
- roles = ckan.new_authz.get_roles_with_permission('edit_group')
+ roles = ckan.new_authz.get_roles_with_permission('manage_group')
if not roles:
return []
user_id = new_authz.get_user_id_for_username(user, allow_none=True)
@@ -551,7 +552,8 @@ def organization_list_for_user(context, data_dict):
orgs_list = model_dictize.group_list_dictize(orgs_q.all(), context)
return orgs_list
-def group_revision_list(context, data_dict):
+
+def _group_or_org_revision_list(context, data_dict):
'''Return a group's revisions.
:param id: the name or id of the group
@@ -566,7 +568,6 @@ def group_revision_list(context, data_dict):
if group is None:
raise NotFound
- _check_access('group_revision_list',context, data_dict)
revision_dicts = []
for revision, object_revisions in group.all_related_revisions:
@@ -575,6 +576,32 @@ def group_revision_list(context, data_dict):
include_groups=False))
return revision_dicts
+def group_revision_list(context, data_dict):
+ '''Return a group's revisions.
+
+ :param id: the name or id of the group
+ :type id: string
+
+ :rtype: list of dictionaries
+
+ '''
+
+ _check_access('group_revision_list',context, data_dict)
+ return _group_or_org_revision_list(context, data_dict)
+
+def organization_revision_list(context, data_dict):
+ '''Return an organization's revisions.
+
+ :param id: the name or id of the organization
+ :type id: string
+
+ :rtype: list of dictionaries
+
+ '''
+
+ _check_access('organization_revision_list',context, data_dict)
+ return _group_or_org_revision_list(context, data_dict)
+
def license_list(context, data_dict):
'''Return the list of licenses available for datasets on the site.
@@ -2329,7 +2356,16 @@ def organization_activity_list_html(context, data_dict):
'''
activity_stream = organization_activity_list(context, data_dict)
- return activity_streams.activity_list_to_html(context, activity_stream)
+ offset = int(data_dict.get('offset', 0))
+ extra_vars = {
+ 'controller': 'organization',
+ 'action': 'activity',
+ 'id': data_dict['id'],
+ 'offset': offset,
+ }
+
+ return activity_streams.activity_list_to_html(context, activity_stream,
+ extra_vars)
def recently_changed_packages_activity_list_html(context, data_dict):
'''Return the activity stream of all recently changed packages as HTML.
@@ -2916,11 +2952,20 @@ def _unpick_search(sort, allowed_fields=None, total=None):
def member_roles_list(context, data_dict):
'''Return the possible roles for members of groups and organizations.
+ :param group_type: the group type, either "group" or "organization"
+ (optional, default "organization")
+ :type id: string
:returns: a list of dictionaries each with two keys: "text" (the display
name of the role, e.g. "Admin") and "value" (the internal name of the
role, e.g. "admin")
:rtype: list of dictionaries
'''
+ group_type = data_dict.get('group_type', 'organization')
+ roles_list = new_authz.roles_list()
+ if group_type == 'group':
+ roles_list = [role for role in roles_list
+ if role['value'] != 'editor']
+
_check_access('member_roles_list', context, data_dict)
- return new_authz.roles_list()
+ return roles_list
diff --git a/ckan/logic/action/update.py b/ckan/logic/action/update.py
index 84895f1d6f7..edf1ddcd548 100644
--- a/ckan/logic/action/update.py
+++ b/ckan/logic/action/update.py
@@ -19,6 +19,7 @@
import ckan.lib.plugins as lib_plugins
import ckan.lib.email_notifications as email_notifications
import ckan.lib.search as search
+import ckan.lib.uploader as uploader
from ckan.common import _, request
@@ -218,15 +219,23 @@ def resource_update(context, data_dict):
else:
logging.error('Could not find resource ' + id)
raise NotFound(_('Resource was not found.'))
+
+ upload = uploader.ResourceUpload(data_dict)
+
pkg_dict['resources'][n] = data_dict
try:
+ context['defer_commit'] = True
+ context['use_cache'] = False
pkg_dict = _get_action('package_update')(context, pkg_dict)
+ context.pop('defer_commit')
except ValidationError, e:
errors = e.error_dict['resources'][n]
raise ValidationError(errors)
- return pkg_dict['resources'][n]
+ upload.upload(id, uploader.get_max_resource_size())
+ model.repo.commit()
+ return _get_action('resource_show')(context, {'id': id})
def package_update(context, data_dict):
@@ -423,6 +432,10 @@ def _group_or_org_update(context, data_dict, is_org=False):
except AttributeError:
schema = group_plugin.form_to_db_schema()
+ upload = uploader.Upload('group', group.image_url)
+ upload.update_data_dict(data_dict, 'image_url',
+ 'image_upload', 'clear_upload')
+
if is_org:
_check_access('organization_update', context, data_dict)
else:
@@ -510,9 +523,11 @@ def _group_or_org_update(context, data_dict, is_org=False):
# TODO: Also create an activity detail recording what exactly changed
# in the group.
+ upload.upload(uploader.get_max_image_size())
if not context.get('defer_commit'):
model.repo.commit()
+
return model_dictize.group_dictize(group, context)
def group_update(context, data_dict):
diff --git a/ckan/logic/auth/create.py b/ckan/logic/auth/create.py
index 792c077670c..0c7be8de7a4 100644
--- a/ckan/logic/auth/create.py
+++ b/ckan/logic/auth/create.py
@@ -1,5 +1,6 @@
import ckan.logic as logic
import ckan.new_authz as new_authz
+import ckan.logic.auth as logic_auth
from ckan.common import _
@@ -103,15 +104,22 @@ def rating_create(context, data_dict):
# No authz check in the logic function
return {'success': True}
+
@logic.auth_allow_anonymous_access
def user_create(context, data_dict=None):
- user = context['user']
-
- if ('api_version' in context
- and not new_authz.check_config_permission('create_user_via_api')):
- return {'success': False, 'msg': _('User %s not authorized to create users') % user}
- else:
- return {'success': True}
+ using_api = 'api_version' in context
+ create_user_via_api = new_authz.check_config_permission(
+ 'create_user_via_api')
+ create_user_via_web = new_authz.check_config_permission(
+ 'create_user_via_web')
+
+ if using_api and not create_user_via_api:
+ return {'success': False, 'msg': _('User {user} not authorized to '
+ 'create users via the API').format(user=context.get('user'))}
+ if not using_api and not create_user_via_web:
+ return {'success': False, 'msg': _('Not authorized to '
+ 'create users')}
+ return {'success': True}
def user_invite(context, data_dict=None):
context['id'] = context.get('group_id')
@@ -208,3 +216,23 @@ def organization_member_create(context, data_dict):
def group_member_create(context, data_dict):
return _group_or_org_member_create(context, data_dict)
+
+def member_create(context, data_dict):
+ group = logic_auth.get_group_object(context, data_dict)
+ user = context['user']
+
+ # User must be able to update the group to add a member to it
+ permission = 'update'
+ # However if the user is member of group then they can add/remove datasets
+ if not group.is_organization and data_dict.get('object_type') == 'package':
+ permission = 'manage_group'
+
+ authorized = new_authz.has_user_permission_for_group_or_org(group.id,
+ user,
+ permission)
+ if not authorized:
+ return {'success': False,
+ 'msg': _('User %s not authorized to edit group %s') %
+ (str(user), group.id)}
+ else:
+ return {'success': True}
diff --git a/ckan/logic/auth/get.py b/ckan/logic/auth/get.py
index c00624bb5d6..37de61d975a 100644
--- a/ckan/logic/auth/get.py
+++ b/ckan/logic/auth/get.py
@@ -39,6 +39,9 @@ def revision_list(context, data_dict):
def group_revision_list(context, data_dict):
return group_show(context, data_dict)
+def organization_revision_list(context, data_dict):
+ return group_show(context, data_dict)
+
def package_revision_list(context, data_dict):
return package_show(context, data_dict)
diff --git a/ckan/logic/schema.py b/ckan/logic/schema.py
index fc54b01990b..901121a95ea 100644
--- a/ckan/logic/schema.py
+++ b/ckan/logic/schema.py
@@ -224,6 +224,8 @@ def default_show_package_schema():
schema['groups'].update({
'description': [ignore_missing],
+ 'display_name': [ignore_missing],
+ 'image_display_url': [ignore_missing],
})
# Remove validators for several keys from the schema so validation doesn't
@@ -264,6 +266,7 @@ def default_group_schema():
'title': [ignore_missing, unicode],
'description': [ignore_missing, unicode],
'image_url': [ignore_missing, unicode],
+ 'image_display_url': [ignore_missing, unicode],
'type': [ignore_missing, unicode],
'state': [ignore_not_group_admin, ignore_missing],
'created': [ignore],
@@ -346,6 +349,7 @@ def default_extras_schema():
'state': [ignore],
'deleted': [ignore_missing],
'revision_timestamp': [ignore],
+ '__extras': [ignore],
}
return schema
diff --git a/ckan/new_authz.py b/ckan/new_authz.py
index e5ac46f8042..44eb09f5dc9 100644
--- a/ckan/new_authz.py
+++ b/ckan/new_authz.py
@@ -83,7 +83,7 @@ def _build(self):
resolved_auth_function_plugins[name]
)
)
- log.debug('Auth function %r was inserted', plugin.name)
+ log.debug('Auth function {0} from plugin {1} was inserted'.format(name, plugin.name))
resolved_auth_function_plugins[name] = plugin.name
fetched_auth_functions[name] = auth_function
# Use the updated ones in preference to the originals.
@@ -96,6 +96,7 @@ def _build(self):
def clear_auth_functions_cache():
_AuthFunctions.clear()
+ CONFIG_PERMISSIONS.clear()
def auth_functions_list():
@@ -191,8 +192,8 @@ def is_authorized(action, context, data_dict=None):
# these are the permissions that roles have
ROLE_PERMISSIONS = OrderedDict([
('admin', ['admin']),
- ('editor', ['read', 'delete_dataset', 'create_dataset', 'update_dataset']),
- ('member', ['read']),
+ ('editor', ['read', 'delete_dataset', 'create_dataset', 'update_dataset', 'manage_group']),
+ ('member', ['read', 'manage_group']),
])
@@ -379,6 +380,7 @@ def get_user_id_for_username(user_name, allow_none=False):
'user_delete_groups': True,
'user_delete_organizations': True,
'create_user_via_api': False,
+ 'create_user_via_web': True,
'roles_that_cascade_to_sub_groups': 'admin',
}
diff --git a/ckan/new_tests/helpers.py b/ckan/new_tests/helpers.py
index 51640a7e3dd..4e1e1ffd0c7 100644
--- a/ckan/new_tests/helpers.py
+++ b/ckan/new_tests/helpers.py
@@ -38,7 +38,7 @@ def reset_db():
# This prevents CKAN from hanging waiting for some unclosed connection.
model.Session.close_all()
- model.repo.clean_db()
+ model.repo.rebuild_db()
def call_action(action_name, context=None, **kwargs):
diff --git a/ckan/plugins/interfaces.py b/ckan/plugins/interfaces.py
index 14d64b01146..f3dfce7d962 100644
--- a/ckan/plugins/interfaces.py
+++ b/ckan/plugins/interfaces.py
@@ -770,7 +770,7 @@ def read_template(self):
If the user requests the dataset in a format other than HTML
(CKAN supports returning datasets in RDF or N3 format by appending .rdf
- or .n3 to the dataset read URL, see :doc:`linked-data-and-rdf`) then
+ or .n3 to the dataset read URL, see :doc:`/linked-data-and-rdf`) then
CKAN will try to render
a template file with the same path as returned by this function,
but a different filename extension, e.g. ``'package/read.rdf'``.
diff --git a/ckan/public/base/css/fuchsia.css b/ckan/public/base/css/fuchsia.css
index c87cd3b10a1..2017ab41be6 100644
--- a/ckan/public/base/css/fuchsia.css
+++ b/ckan/public/base/css/fuchsia.css
@@ -49,16 +49,12 @@ sub {
}
img {
/* Responsive images (ensure images don't scale beyond their parents) */
-
max-width: 100%;
/* Part 1: Set a maxium relative to the parent */
-
width: auto\9;
/* IE7-8 need help adjusting responsive images */
-
height: auto;
/* Part 2: Scale the height according to the width, otherwise you get stretching */
-
vertical-align: middle;
border: 0;
-ms-interpolation-mode: bicubic;
@@ -153,7 +149,7 @@ textarea {
img {
max-width: 100% !important;
}
- @page {
+ @page {
margin: 0.5cm;
}
p,
@@ -693,7 +689,6 @@ ol.inline > li {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
padding-left: 5px;
padding-right: 5px;
@@ -972,7 +967,6 @@ input[type="color"]:focus,
outline: 0;
outline: thin dotted \9;
/* IE6-9 */
-
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
-moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
@@ -982,10 +976,8 @@ input[type="checkbox"] {
margin: 4px 0 0;
*margin-top: 0;
/* IE7 */
-
margin-top: 1px \9;
/* IE8-9 */
-
line-height: normal;
}
input[type="file"],
@@ -1001,10 +993,8 @@ select,
input[type="file"] {
height: 30px;
/* In IE7, the height of the select element cannot be changed by height, only font-size */
-
*margin-top: 4px;
/* For IE7, add top margin to align select with labels */
-
line-height: 30px;
}
select {
@@ -1402,7 +1392,6 @@ select:focus:invalid:focus {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
vertical-align: middle;
padding-left: 5px;
@@ -1553,7 +1542,6 @@ input.search-query {
padding-left: 14px;
padding-left: 4px \9;
/* IE7-8 doesn't have border-radius, so don't indent the padding */
-
margin-bottom: 0;
-webkit-border-radius: 15px;
-moz-border-radius: 15px;
@@ -1610,7 +1598,6 @@ input.search-query {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
margin-bottom: 0;
vertical-align: middle;
@@ -2220,7 +2207,6 @@ button.close {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
padding: 4px 12px;
margin-bottom: 0;
@@ -2243,7 +2229,6 @@ button.close {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #eaeaea;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
border: 1px solid #cccccc;
*border: 0;
@@ -2379,7 +2364,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #e73892;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-primary:hover,
@@ -2411,7 +2395,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #f89406;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-warning:hover,
@@ -2443,7 +2426,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #bd362f;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-danger:hover,
@@ -2475,7 +2457,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #51a351;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-success:hover,
@@ -2507,7 +2488,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #2f96b4;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-info:hover,
@@ -2539,7 +2519,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #222222;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-inverse:hover,
@@ -2614,7 +2593,6 @@ input[type="submit"].btn.btn-mini {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
font-size: 0;
vertical-align: middle;
@@ -2790,7 +2768,6 @@ input[type="submit"].btn.btn-mini {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
}
.btn-group-vertical > .btn {
@@ -3487,7 +3464,6 @@ input[type="submit"].btn.btn-mini {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #e5e5e5;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
-webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.075);
-moz-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.075);
@@ -3721,7 +3697,6 @@ input[type="submit"].btn.btn-mini {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #040404;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.navbar-inverse .btn-navbar:hover,
@@ -3751,7 +3726,6 @@ input[type="submit"].btn.btn-mini {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
text-shadow: 0 1px 0 #ffffff;
}
@@ -3769,7 +3743,6 @@ input[type="submit"].btn.btn-mini {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
margin-left: 0;
margin-bottom: 0;
@@ -3970,7 +3943,6 @@ input[type="submit"].btn.btn-mini {
border: 1px solid rgba(0, 0, 0, 0.3);
*border: 1px solid #999;
/* IE6-7 */
-
-webkit-border-radius: 6px;
-moz-border-radius: 6px;
border-radius: 6px;
@@ -5575,6 +5547,12 @@ textarea {
}
.form-actions .control-required-message {
float: left;
+ margin-left: 20px;
+ margin-bottom: 0;
+ line-height: 30px;
+}
+.form-actions .control-required-message:first-child {
+ margin-left: 0;
}
.form-actions {
background: none;
@@ -5692,27 +5670,24 @@ textarea {
padding: 7px 5px;
}
.simple-input .field .btn-search {
- *margin-right: .3em;
- display: inline-block;
- vertical-align: text-bottom;
- position: relative;
- top: 2px;
- width: 16px;
- height: 16px;
- background-image: url("../../../base/images/sprite-ckan-icons.png");
- background-repeat: no-repeat;
- background-position: 16px 16px;
- background-position: -51px -16px;
position: absolute;
display: block;
height: 17px;
width: 17px;
+ padding: 0;
top: 50%;
right: 0;
- margin-top: -8px;
+ margin-top: -10px;
background-color: transparent;
border: none;
- text-indent: -999em;
+ color: #999;
+ -webkit-transition: color 0.2s ease-in;
+ -moz-transition: color 0.2s ease-in;
+ -o-transition: color 0.2s ease-in;
+ transition: color 0.2s ease-in;
+}
+.simple-input .field .btn-search:hover {
+ color: #000;
}
.editor textarea {
-webkit-border-radius: 3px 3px 0 0;
@@ -5818,7 +5793,6 @@ textarea {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #e73892;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.control-custom.disabled .checkbox.btn:hover,
@@ -6097,7 +6071,6 @@ textarea {
outline: 0;
outline: thin dotted \9;
/* IE6-9 */
-
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
-moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
@@ -6120,6 +6093,43 @@ textarea {
-moz-box-shadow: none;
box-shadow: none;
}
+.js .image-upload #field-image-upload {
+ cursor: pointer;
+ position: absolute;
+ z-index: 1;
+ opacity: 0;
+ filter: alpha(opacity=0);
+}
+.js .image-upload .controls {
+ position: relative;
+}
+.js .image-upload .btn {
+ position: relative;
+ top: 0;
+ margin-right: 10px;
+}
+.js .image-upload .btn.hover {
+ color: #333333;
+ text-decoration: none;
+ background-position: 0 -15px;
+ -webkit-transition: background-position 0.1s linear;
+ -moz-transition: background-position 0.1s linear;
+ -o-transition: background-position 0.1s linear;
+ transition: background-position 0.1s linear;
+}
+.js .image-upload .btn-remove-url {
+ position: absolute;
+ margin-right: 0;
+ top: 4px;
+ right: 5px;
+ padding: 0 4px;
+ -webkit-border-radius: 100px;
+ -moz-border-radius: 100px;
+ border-radius: 100px;
+}
+.js .image-upload .btn-remove-url .icon-remove {
+ margin-right: 0;
+}
.add-member-form .control-label {
width: 100%;
text-align: left;
@@ -6414,6 +6424,10 @@ textarea {
font-weight: normal;
color: #000000;
}
+.search-form.no-bottom-border {
+ border-bottom-width: 0;
+ margin-bottom: 0;
+}
.tertiary .control-order-by {
float: none;
margin: 0;
@@ -6534,7 +6548,6 @@ textarea {
margin-right: 5px;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
}
.actions li:last-of-type {
@@ -7159,6 +7172,11 @@ h4 small {
.context-info.editing .module-content {
margin-top: 0;
}
+.flash-messages .alert {
+ -webkit-box-shadow: 0 0 0 1px #ffffff;
+ -moz-box-shadow: 0 0 0 1px #ffffff;
+ box-shadow: 0 0 0 1px #ffffff;
+}
.homepage [role=main] {
padding: 20px 0;
min-height: 0;
@@ -7711,6 +7729,9 @@ h4 small {
-moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
+.activity .item.no-avatar p {
+ margin-left: 40px;
+}
.activity .load-less {
margin-bottom: 15px;
}
@@ -7728,9 +7749,28 @@ h4 small {
float: right;
text-decoration: none;
}
+.popover .popover-content {
+ font-size: 14px;
+ line-height: 20px;
+ color: #444444;
+ word-break: break-all;
+}
+.popover .popover-content dl {
+ margin: 0;
+}
+.popover .popover-content dl dd {
+ margin-left: 0;
+ margin-bottom: 10px;
+}
.activity .item .icon {
background-color: #999999;
}
+.activity .item.failure .icon {
+ background-color: #b95252;
+}
+.activity .item.success .icon {
+ background-color: #69a67a;
+}
.activity .item.added-tag .icon {
background-color: #6995a6;
}
@@ -7981,6 +8021,21 @@ h4 small {
font-size: 16px;
margin: 3px 0;
}
+.datapusher-status-link:hover {
+ text-decoration: none;
+}
+.datapusher-status.status-unknown {
+ color: #bbb;
+}
+.datapusher-status.status-pending {
+ color: #FFCC00;
+}
+.datapusher-status.status-error {
+ color: red;
+}
+.datapusher-status.status-complete {
+ color: #009900;
+}
body {
background: #e73892 url("../../../base/images/bg.png");
}
@@ -8182,7 +8237,6 @@ iframe {
.ie7 .control-custom .checkbox {
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
}
.ie7 .stages {
@@ -8216,7 +8270,6 @@ iframe {
.ie7 .masthead nav {
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
}
.ie7 .masthead .header-image {
diff --git a/ckan/public/base/css/green.css b/ckan/public/base/css/green.css
index 6558ce82f8d..0b6f45d9109 100644
--- a/ckan/public/base/css/green.css
+++ b/ckan/public/base/css/green.css
@@ -49,16 +49,12 @@ sub {
}
img {
/* Responsive images (ensure images don't scale beyond their parents) */
-
max-width: 100%;
/* Part 1: Set a maxium relative to the parent */
-
width: auto\9;
/* IE7-8 need help adjusting responsive images */
-
height: auto;
/* Part 2: Scale the height according to the width, otherwise you get stretching */
-
vertical-align: middle;
border: 0;
-ms-interpolation-mode: bicubic;
@@ -153,7 +149,7 @@ textarea {
img {
max-width: 100% !important;
}
- @page {
+ @page {
margin: 0.5cm;
}
p,
@@ -693,7 +689,6 @@ ol.inline > li {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
padding-left: 5px;
padding-right: 5px;
@@ -972,7 +967,6 @@ input[type="color"]:focus,
outline: 0;
outline: thin dotted \9;
/* IE6-9 */
-
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
-moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
@@ -982,10 +976,8 @@ input[type="checkbox"] {
margin: 4px 0 0;
*margin-top: 0;
/* IE7 */
-
margin-top: 1px \9;
/* IE8-9 */
-
line-height: normal;
}
input[type="file"],
@@ -1001,10 +993,8 @@ select,
input[type="file"] {
height: 30px;
/* In IE7, the height of the select element cannot be changed by height, only font-size */
-
*margin-top: 4px;
/* For IE7, add top margin to align select with labels */
-
line-height: 30px;
}
select {
@@ -1402,7 +1392,6 @@ select:focus:invalid:focus {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
vertical-align: middle;
padding-left: 5px;
@@ -1553,7 +1542,6 @@ input.search-query {
padding-left: 14px;
padding-left: 4px \9;
/* IE7-8 doesn't have border-radius, so don't indent the padding */
-
margin-bottom: 0;
-webkit-border-radius: 15px;
-moz-border-radius: 15px;
@@ -1610,7 +1598,6 @@ input.search-query {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
margin-bottom: 0;
vertical-align: middle;
@@ -2220,7 +2207,6 @@ button.close {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
padding: 4px 12px;
margin-bottom: 0;
@@ -2243,7 +2229,6 @@ button.close {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #eaeaea;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
border: 1px solid #cccccc;
*border: 0;
@@ -2379,7 +2364,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #2f9b45;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-primary:hover,
@@ -2411,7 +2395,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #f89406;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-warning:hover,
@@ -2443,7 +2426,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #bd362f;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-danger:hover,
@@ -2475,7 +2457,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #51a351;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-success:hover,
@@ -2507,7 +2488,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #2f96b4;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-info:hover,
@@ -2539,7 +2519,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #222222;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-inverse:hover,
@@ -2614,7 +2593,6 @@ input[type="submit"].btn.btn-mini {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
font-size: 0;
vertical-align: middle;
@@ -2790,7 +2768,6 @@ input[type="submit"].btn.btn-mini {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
}
.btn-group-vertical > .btn {
@@ -3487,7 +3464,6 @@ input[type="submit"].btn.btn-mini {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #e5e5e5;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
-webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.075);
-moz-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.075);
@@ -3721,7 +3697,6 @@ input[type="submit"].btn.btn-mini {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #040404;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.navbar-inverse .btn-navbar:hover,
@@ -3751,7 +3726,6 @@ input[type="submit"].btn.btn-mini {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
text-shadow: 0 1px 0 #ffffff;
}
@@ -3769,7 +3743,6 @@ input[type="submit"].btn.btn-mini {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
margin-left: 0;
margin-bottom: 0;
@@ -3970,7 +3943,6 @@ input[type="submit"].btn.btn-mini {
border: 1px solid rgba(0, 0, 0, 0.3);
*border: 1px solid #999;
/* IE6-7 */
-
-webkit-border-radius: 6px;
-moz-border-radius: 6px;
border-radius: 6px;
@@ -5575,6 +5547,12 @@ textarea {
}
.form-actions .control-required-message {
float: left;
+ margin-left: 20px;
+ margin-bottom: 0;
+ line-height: 30px;
+}
+.form-actions .control-required-message:first-child {
+ margin-left: 0;
}
.form-actions {
background: none;
@@ -5692,27 +5670,24 @@ textarea {
padding: 7px 5px;
}
.simple-input .field .btn-search {
- *margin-right: .3em;
- display: inline-block;
- vertical-align: text-bottom;
- position: relative;
- top: 2px;
- width: 16px;
- height: 16px;
- background-image: url("../../../base/images/sprite-ckan-icons.png");
- background-repeat: no-repeat;
- background-position: 16px 16px;
- background-position: -51px -16px;
position: absolute;
display: block;
height: 17px;
width: 17px;
+ padding: 0;
top: 50%;
right: 0;
- margin-top: -8px;
+ margin-top: -10px;
background-color: transparent;
border: none;
- text-indent: -999em;
+ color: #999;
+ -webkit-transition: color 0.2s ease-in;
+ -moz-transition: color 0.2s ease-in;
+ -o-transition: color 0.2s ease-in;
+ transition: color 0.2s ease-in;
+}
+.simple-input .field .btn-search:hover {
+ color: #000;
}
.editor textarea {
-webkit-border-radius: 3px 3px 0 0;
@@ -5818,7 +5793,6 @@ textarea {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #2f9b45;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.control-custom.disabled .checkbox.btn:hover,
@@ -6097,7 +6071,6 @@ textarea {
outline: 0;
outline: thin dotted \9;
/* IE6-9 */
-
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
-moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
@@ -6120,6 +6093,43 @@ textarea {
-moz-box-shadow: none;
box-shadow: none;
}
+.js .image-upload #field-image-upload {
+ cursor: pointer;
+ position: absolute;
+ z-index: 1;
+ opacity: 0;
+ filter: alpha(opacity=0);
+}
+.js .image-upload .controls {
+ position: relative;
+}
+.js .image-upload .btn {
+ position: relative;
+ top: 0;
+ margin-right: 10px;
+}
+.js .image-upload .btn.hover {
+ color: #333333;
+ text-decoration: none;
+ background-position: 0 -15px;
+ -webkit-transition: background-position 0.1s linear;
+ -moz-transition: background-position 0.1s linear;
+ -o-transition: background-position 0.1s linear;
+ transition: background-position 0.1s linear;
+}
+.js .image-upload .btn-remove-url {
+ position: absolute;
+ margin-right: 0;
+ top: 4px;
+ right: 5px;
+ padding: 0 4px;
+ -webkit-border-radius: 100px;
+ -moz-border-radius: 100px;
+ border-radius: 100px;
+}
+.js .image-upload .btn-remove-url .icon-remove {
+ margin-right: 0;
+}
.add-member-form .control-label {
width: 100%;
text-align: left;
@@ -6414,6 +6424,10 @@ textarea {
font-weight: normal;
color: #000000;
}
+.search-form.no-bottom-border {
+ border-bottom-width: 0;
+ margin-bottom: 0;
+}
.tertiary .control-order-by {
float: none;
margin: 0;
@@ -6534,7 +6548,6 @@ textarea {
margin-right: 5px;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
}
.actions li:last-of-type {
@@ -7159,6 +7172,11 @@ h4 small {
.context-info.editing .module-content {
margin-top: 0;
}
+.flash-messages .alert {
+ -webkit-box-shadow: 0 0 0 1px #ffffff;
+ -moz-box-shadow: 0 0 0 1px #ffffff;
+ box-shadow: 0 0 0 1px #ffffff;
+}
.homepage [role=main] {
padding: 20px 0;
min-height: 0;
@@ -7711,6 +7729,9 @@ h4 small {
-moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
+.activity .item.no-avatar p {
+ margin-left: 40px;
+}
.activity .load-less {
margin-bottom: 15px;
}
@@ -7728,9 +7749,28 @@ h4 small {
float: right;
text-decoration: none;
}
+.popover .popover-content {
+ font-size: 14px;
+ line-height: 20px;
+ color: #444444;
+ word-break: break-all;
+}
+.popover .popover-content dl {
+ margin: 0;
+}
+.popover .popover-content dl dd {
+ margin-left: 0;
+ margin-bottom: 10px;
+}
.activity .item .icon {
background-color: #999999;
}
+.activity .item.failure .icon {
+ background-color: #b95252;
+}
+.activity .item.success .icon {
+ background-color: #69a67a;
+}
.activity .item.added-tag .icon {
background-color: #6995a6;
}
@@ -7981,6 +8021,21 @@ h4 small {
font-size: 16px;
margin: 3px 0;
}
+.datapusher-status-link:hover {
+ text-decoration: none;
+}
+.datapusher-status.status-unknown {
+ color: #bbb;
+}
+.datapusher-status.status-pending {
+ color: #FFCC00;
+}
+.datapusher-status.status-error {
+ color: red;
+}
+.datapusher-status.status-complete {
+ color: #009900;
+}
body {
background: #2f9b45 url("../../../base/images/bg.png");
}
@@ -8182,7 +8237,6 @@ iframe {
.ie7 .control-custom .checkbox {
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
}
.ie7 .stages {
@@ -8216,7 +8270,6 @@ iframe {
.ie7 .masthead nav {
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
}
.ie7 .masthead .header-image {
diff --git a/ckan/public/base/css/main.css b/ckan/public/base/css/main.css
index d7635468324..df4c0110922 100644
--- a/ckan/public/base/css/main.css
+++ b/ckan/public/base/css/main.css
@@ -49,16 +49,12 @@ sub {
}
img {
/* Responsive images (ensure images don't scale beyond their parents) */
-
max-width: 100%;
/* Part 1: Set a maxium relative to the parent */
-
width: auto\9;
/* IE7-8 need help adjusting responsive images */
-
height: auto;
/* Part 2: Scale the height according to the width, otherwise you get stretching */
-
vertical-align: middle;
border: 0;
-ms-interpolation-mode: bicubic;
@@ -153,7 +149,7 @@ textarea {
img {
max-width: 100% !important;
}
- @page {
+ @page {
margin: 0.5cm;
}
p,
@@ -693,7 +689,6 @@ ol.inline > li {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
padding-left: 5px;
padding-right: 5px;
@@ -972,7 +967,6 @@ input[type="color"]:focus,
outline: 0;
outline: thin dotted \9;
/* IE6-9 */
-
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
-moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
@@ -982,10 +976,8 @@ input[type="checkbox"] {
margin: 4px 0 0;
*margin-top: 0;
/* IE7 */
-
margin-top: 1px \9;
/* IE8-9 */
-
line-height: normal;
}
input[type="file"],
@@ -1001,10 +993,8 @@ select,
input[type="file"] {
height: 30px;
/* In IE7, the height of the select element cannot be changed by height, only font-size */
-
*margin-top: 4px;
/* For IE7, add top margin to align select with labels */
-
line-height: 30px;
}
select {
@@ -1402,7 +1392,6 @@ select:focus:invalid:focus {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
vertical-align: middle;
padding-left: 5px;
@@ -1553,7 +1542,6 @@ input.search-query {
padding-left: 14px;
padding-left: 4px \9;
/* IE7-8 doesn't have border-radius, so don't indent the padding */
-
margin-bottom: 0;
-webkit-border-radius: 15px;
-moz-border-radius: 15px;
@@ -1610,7 +1598,6 @@ input.search-query {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
margin-bottom: 0;
vertical-align: middle;
@@ -2220,7 +2207,6 @@ button.close {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
padding: 4px 12px;
margin-bottom: 0;
@@ -2243,7 +2229,6 @@ button.close {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #eaeaea;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
border: 1px solid #cccccc;
*border: 0;
@@ -2379,7 +2364,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #085871;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-primary:hover,
@@ -2411,7 +2395,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #f89406;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-warning:hover,
@@ -2443,7 +2426,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #bd362f;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-danger:hover,
@@ -2475,7 +2457,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #51a351;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-success:hover,
@@ -2507,7 +2488,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #2f96b4;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-info:hover,
@@ -2539,7 +2519,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #222222;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-inverse:hover,
@@ -2614,7 +2593,6 @@ input[type="submit"].btn.btn-mini {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
font-size: 0;
vertical-align: middle;
@@ -2790,7 +2768,6 @@ input[type="submit"].btn.btn-mini {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
}
.btn-group-vertical > .btn {
@@ -3487,7 +3464,6 @@ input[type="submit"].btn.btn-mini {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #e5e5e5;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
-webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.075);
-moz-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.075);
@@ -3721,7 +3697,6 @@ input[type="submit"].btn.btn-mini {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #040404;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.navbar-inverse .btn-navbar:hover,
@@ -3751,7 +3726,6 @@ input[type="submit"].btn.btn-mini {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
text-shadow: 0 1px 0 #ffffff;
}
@@ -3769,7 +3743,6 @@ input[type="submit"].btn.btn-mini {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
margin-left: 0;
margin-bottom: 0;
@@ -3970,7 +3943,6 @@ input[type="submit"].btn.btn-mini {
border: 1px solid rgba(0, 0, 0, 0.3);
*border: 1px solid #999;
/* IE6-7 */
-
-webkit-border-radius: 6px;
-moz-border-radius: 6px;
border-radius: 6px;
@@ -5575,6 +5547,12 @@ textarea {
}
.form-actions .control-required-message {
float: left;
+ margin-left: 20px;
+ margin-bottom: 0;
+ line-height: 30px;
+}
+.form-actions .control-required-message:first-child {
+ margin-left: 0;
}
.form-actions {
background: none;
@@ -5692,27 +5670,24 @@ textarea {
padding: 7px 5px;
}
.simple-input .field .btn-search {
- *margin-right: .3em;
- display: inline-block;
- vertical-align: text-bottom;
- position: relative;
- top: 2px;
- width: 16px;
- height: 16px;
- background-image: url("../../../base/images/sprite-ckan-icons.png");
- background-repeat: no-repeat;
- background-position: 16px 16px;
- background-position: -51px -16px;
position: absolute;
display: block;
height: 17px;
width: 17px;
+ padding: 0;
top: 50%;
right: 0;
- margin-top: -8px;
+ margin-top: -10px;
background-color: transparent;
border: none;
- text-indent: -999em;
+ color: #999;
+ -webkit-transition: color 0.2s ease-in;
+ -moz-transition: color 0.2s ease-in;
+ -o-transition: color 0.2s ease-in;
+ transition: color 0.2s ease-in;
+}
+.simple-input .field .btn-search:hover {
+ color: #000;
}
.editor textarea {
-webkit-border-radius: 3px 3px 0 0;
@@ -5818,7 +5793,6 @@ textarea {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #085871;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.control-custom.disabled .checkbox.btn:hover,
@@ -6097,7 +6071,6 @@ textarea {
outline: 0;
outline: thin dotted \9;
/* IE6-9 */
-
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
-moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
@@ -6120,6 +6093,43 @@ textarea {
-moz-box-shadow: none;
box-shadow: none;
}
+.js .image-upload #field-image-upload {
+ cursor: pointer;
+ position: absolute;
+ z-index: 1;
+ opacity: 0;
+ filter: alpha(opacity=0);
+}
+.js .image-upload .controls {
+ position: relative;
+}
+.js .image-upload .btn {
+ position: relative;
+ top: 0;
+ margin-right: 10px;
+}
+.js .image-upload .btn.hover {
+ color: #333333;
+ text-decoration: none;
+ background-position: 0 -15px;
+ -webkit-transition: background-position 0.1s linear;
+ -moz-transition: background-position 0.1s linear;
+ -o-transition: background-position 0.1s linear;
+ transition: background-position 0.1s linear;
+}
+.js .image-upload .btn-remove-url {
+ position: absolute;
+ margin-right: 0;
+ top: 4px;
+ right: 5px;
+ padding: 0 4px;
+ -webkit-border-radius: 100px;
+ -moz-border-radius: 100px;
+ border-radius: 100px;
+}
+.js .image-upload .btn-remove-url .icon-remove {
+ margin-right: 0;
+}
.add-member-form .control-label {
width: 100%;
text-align: left;
@@ -6414,6 +6424,10 @@ textarea {
font-weight: normal;
color: #000000;
}
+.search-form.no-bottom-border {
+ border-bottom-width: 0;
+ margin-bottom: 0;
+}
.tertiary .control-order-by {
float: none;
margin: 0;
@@ -6534,7 +6548,6 @@ textarea {
margin-right: 5px;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
}
.actions li:last-of-type {
@@ -7159,6 +7172,11 @@ h4 small {
.context-info.editing .module-content {
margin-top: 0;
}
+.flash-messages .alert {
+ -webkit-box-shadow: 0 0 0 1px #ffffff;
+ -moz-box-shadow: 0 0 0 1px #ffffff;
+ box-shadow: 0 0 0 1px #ffffff;
+}
.homepage [role=main] {
padding: 20px 0;
min-height: 0;
@@ -7711,6 +7729,9 @@ h4 small {
-moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
+.activity .item.no-avatar p {
+ margin-left: 40px;
+}
.activity .load-less {
margin-bottom: 15px;
}
@@ -7728,9 +7749,28 @@ h4 small {
float: right;
text-decoration: none;
}
+.popover .popover-content {
+ font-size: 14px;
+ line-height: 20px;
+ color: #444444;
+ word-break: break-all;
+}
+.popover .popover-content dl {
+ margin: 0;
+}
+.popover .popover-content dl dd {
+ margin-left: 0;
+ margin-bottom: 10px;
+}
.activity .item .icon {
background-color: #999999;
}
+.activity .item.failure .icon {
+ background-color: #b95252;
+}
+.activity .item.success .icon {
+ background-color: #69a67a;
+}
.activity .item.added-tag .icon {
background-color: #6995a6;
}
@@ -7981,6 +8021,21 @@ h4 small {
font-size: 16px;
margin: 3px 0;
}
+.datapusher-status-link:hover {
+ text-decoration: none;
+}
+.datapusher-status.status-unknown {
+ color: #bbb;
+}
+.datapusher-status.status-pending {
+ color: #FFCC00;
+}
+.datapusher-status.status-error {
+ color: red;
+}
+.datapusher-status.status-complete {
+ color: #009900;
+}
body {
background: #005d7a url("../../../base/images/bg.png");
}
@@ -8182,7 +8237,6 @@ iframe {
.ie7 .control-custom .checkbox {
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
}
.ie7 .stages {
@@ -8216,7 +8270,6 @@ iframe {
.ie7 .masthead nav {
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
}
.ie7 .masthead .header-image {
diff --git a/ckan/public/base/css/maroon.css b/ckan/public/base/css/maroon.css
index 4de9736ff0e..c93b5426b72 100644
--- a/ckan/public/base/css/maroon.css
+++ b/ckan/public/base/css/maroon.css
@@ -49,16 +49,12 @@ sub {
}
img {
/* Responsive images (ensure images don't scale beyond their parents) */
-
max-width: 100%;
/* Part 1: Set a maxium relative to the parent */
-
width: auto\9;
/* IE7-8 need help adjusting responsive images */
-
height: auto;
/* Part 2: Scale the height according to the width, otherwise you get stretching */
-
vertical-align: middle;
border: 0;
-ms-interpolation-mode: bicubic;
@@ -153,7 +149,7 @@ textarea {
img {
max-width: 100% !important;
}
- @page {
+ @page {
margin: 0.5cm;
}
p,
@@ -693,7 +689,6 @@ ol.inline > li {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
padding-left: 5px;
padding-right: 5px;
@@ -972,7 +967,6 @@ input[type="color"]:focus,
outline: 0;
outline: thin dotted \9;
/* IE6-9 */
-
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
-moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
@@ -982,10 +976,8 @@ input[type="checkbox"] {
margin: 4px 0 0;
*margin-top: 0;
/* IE7 */
-
margin-top: 1px \9;
/* IE8-9 */
-
line-height: normal;
}
input[type="file"],
@@ -1001,10 +993,8 @@ select,
input[type="file"] {
height: 30px;
/* In IE7, the height of the select element cannot be changed by height, only font-size */
-
*margin-top: 4px;
/* For IE7, add top margin to align select with labels */
-
line-height: 30px;
}
select {
@@ -1402,7 +1392,6 @@ select:focus:invalid:focus {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
vertical-align: middle;
padding-left: 5px;
@@ -1553,7 +1542,6 @@ input.search-query {
padding-left: 14px;
padding-left: 4px \9;
/* IE7-8 doesn't have border-radius, so don't indent the padding */
-
margin-bottom: 0;
-webkit-border-radius: 15px;
-moz-border-radius: 15px;
@@ -1610,7 +1598,6 @@ input.search-query {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
margin-bottom: 0;
vertical-align: middle;
@@ -2220,7 +2207,6 @@ button.close {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
padding: 4px 12px;
margin-bottom: 0;
@@ -2243,7 +2229,6 @@ button.close {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #eaeaea;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
border: 1px solid #cccccc;
*border: 0;
@@ -2379,7 +2364,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #810606;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-primary:hover,
@@ -2411,7 +2395,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #f89406;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-warning:hover,
@@ -2443,7 +2426,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #bd362f;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-danger:hover,
@@ -2475,7 +2457,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #51a351;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-success:hover,
@@ -2507,7 +2488,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #2f96b4;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-info:hover,
@@ -2539,7 +2519,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #222222;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-inverse:hover,
@@ -2614,7 +2593,6 @@ input[type="submit"].btn.btn-mini {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
font-size: 0;
vertical-align: middle;
@@ -2790,7 +2768,6 @@ input[type="submit"].btn.btn-mini {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
}
.btn-group-vertical > .btn {
@@ -3487,7 +3464,6 @@ input[type="submit"].btn.btn-mini {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #e5e5e5;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
-webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.075);
-moz-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.075);
@@ -3721,7 +3697,6 @@ input[type="submit"].btn.btn-mini {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #040404;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.navbar-inverse .btn-navbar:hover,
@@ -3751,7 +3726,6 @@ input[type="submit"].btn.btn-mini {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
text-shadow: 0 1px 0 #ffffff;
}
@@ -3769,7 +3743,6 @@ input[type="submit"].btn.btn-mini {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
margin-left: 0;
margin-bottom: 0;
@@ -3970,7 +3943,6 @@ input[type="submit"].btn.btn-mini {
border: 1px solid rgba(0, 0, 0, 0.3);
*border: 1px solid #999;
/* IE6-7 */
-
-webkit-border-radius: 6px;
-moz-border-radius: 6px;
border-radius: 6px;
@@ -5575,6 +5547,12 @@ textarea {
}
.form-actions .control-required-message {
float: left;
+ margin-left: 20px;
+ margin-bottom: 0;
+ line-height: 30px;
+}
+.form-actions .control-required-message:first-child {
+ margin-left: 0;
}
.form-actions {
background: none;
@@ -5692,27 +5670,24 @@ textarea {
padding: 7px 5px;
}
.simple-input .field .btn-search {
- *margin-right: .3em;
- display: inline-block;
- vertical-align: text-bottom;
- position: relative;
- top: 2px;
- width: 16px;
- height: 16px;
- background-image: url("../../../base/images/sprite-ckan-icons.png");
- background-repeat: no-repeat;
- background-position: 16px 16px;
- background-position: -51px -16px;
position: absolute;
display: block;
height: 17px;
width: 17px;
+ padding: 0;
top: 50%;
right: 0;
- margin-top: -8px;
+ margin-top: -10px;
background-color: transparent;
border: none;
- text-indent: -999em;
+ color: #999;
+ -webkit-transition: color 0.2s ease-in;
+ -moz-transition: color 0.2s ease-in;
+ -o-transition: color 0.2s ease-in;
+ transition: color 0.2s ease-in;
+}
+.simple-input .field .btn-search:hover {
+ color: #000;
}
.editor textarea {
-webkit-border-radius: 3px 3px 0 0;
@@ -5818,7 +5793,6 @@ textarea {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #810606;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.control-custom.disabled .checkbox.btn:hover,
@@ -6097,7 +6071,6 @@ textarea {
outline: 0;
outline: thin dotted \9;
/* IE6-9 */
-
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
-moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
@@ -6120,6 +6093,43 @@ textarea {
-moz-box-shadow: none;
box-shadow: none;
}
+.js .image-upload #field-image-upload {
+ cursor: pointer;
+ position: absolute;
+ z-index: 1;
+ opacity: 0;
+ filter: alpha(opacity=0);
+}
+.js .image-upload .controls {
+ position: relative;
+}
+.js .image-upload .btn {
+ position: relative;
+ top: 0;
+ margin-right: 10px;
+}
+.js .image-upload .btn.hover {
+ color: #333333;
+ text-decoration: none;
+ background-position: 0 -15px;
+ -webkit-transition: background-position 0.1s linear;
+ -moz-transition: background-position 0.1s linear;
+ -o-transition: background-position 0.1s linear;
+ transition: background-position 0.1s linear;
+}
+.js .image-upload .btn-remove-url {
+ position: absolute;
+ margin-right: 0;
+ top: 4px;
+ right: 5px;
+ padding: 0 4px;
+ -webkit-border-radius: 100px;
+ -moz-border-radius: 100px;
+ border-radius: 100px;
+}
+.js .image-upload .btn-remove-url .icon-remove {
+ margin-right: 0;
+}
.add-member-form .control-label {
width: 100%;
text-align: left;
@@ -6414,6 +6424,10 @@ textarea {
font-weight: normal;
color: #000000;
}
+.search-form.no-bottom-border {
+ border-bottom-width: 0;
+ margin-bottom: 0;
+}
.tertiary .control-order-by {
float: none;
margin: 0;
@@ -6534,7 +6548,6 @@ textarea {
margin-right: 5px;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
}
.actions li:last-of-type {
@@ -7159,6 +7172,11 @@ h4 small {
.context-info.editing .module-content {
margin-top: 0;
}
+.flash-messages .alert {
+ -webkit-box-shadow: 0 0 0 1px #ffffff;
+ -moz-box-shadow: 0 0 0 1px #ffffff;
+ box-shadow: 0 0 0 1px #ffffff;
+}
.homepage [role=main] {
padding: 20px 0;
min-height: 0;
@@ -7711,6 +7729,9 @@ h4 small {
-moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
+.activity .item.no-avatar p {
+ margin-left: 40px;
+}
.activity .load-less {
margin-bottom: 15px;
}
@@ -7728,9 +7749,28 @@ h4 small {
float: right;
text-decoration: none;
}
+.popover .popover-content {
+ font-size: 14px;
+ line-height: 20px;
+ color: #444444;
+ word-break: break-all;
+}
+.popover .popover-content dl {
+ margin: 0;
+}
+.popover .popover-content dl dd {
+ margin-left: 0;
+ margin-bottom: 10px;
+}
.activity .item .icon {
background-color: #999999;
}
+.activity .item.failure .icon {
+ background-color: #b95252;
+}
+.activity .item.success .icon {
+ background-color: #69a67a;
+}
.activity .item.added-tag .icon {
background-color: #6995a6;
}
@@ -7981,6 +8021,21 @@ h4 small {
font-size: 16px;
margin: 3px 0;
}
+.datapusher-status-link:hover {
+ text-decoration: none;
+}
+.datapusher-status.status-unknown {
+ color: #bbb;
+}
+.datapusher-status.status-pending {
+ color: #FFCC00;
+}
+.datapusher-status.status-error {
+ color: red;
+}
+.datapusher-status.status-complete {
+ color: #009900;
+}
body {
background: #810606 url("../../../base/images/bg.png");
}
@@ -8182,7 +8237,6 @@ iframe {
.ie7 .control-custom .checkbox {
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
}
.ie7 .stages {
@@ -8216,7 +8270,6 @@ iframe {
.ie7 .masthead nav {
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
}
.ie7 .masthead .header-image {
diff --git a/ckan/public/base/css/red.css b/ckan/public/base/css/red.css
index 4b1a9181328..975d4d07065 100644
--- a/ckan/public/base/css/red.css
+++ b/ckan/public/base/css/red.css
@@ -49,16 +49,12 @@ sub {
}
img {
/* Responsive images (ensure images don't scale beyond their parents) */
-
max-width: 100%;
/* Part 1: Set a maxium relative to the parent */
-
width: auto\9;
/* IE7-8 need help adjusting responsive images */
-
height: auto;
/* Part 2: Scale the height according to the width, otherwise you get stretching */
-
vertical-align: middle;
border: 0;
-ms-interpolation-mode: bicubic;
@@ -153,7 +149,7 @@ textarea {
img {
max-width: 100% !important;
}
- @page {
+ @page {
margin: 0.5cm;
}
p,
@@ -693,7 +689,6 @@ ol.inline > li {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
padding-left: 5px;
padding-right: 5px;
@@ -972,7 +967,6 @@ input[type="color"]:focus,
outline: 0;
outline: thin dotted \9;
/* IE6-9 */
-
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
-moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
@@ -982,10 +976,8 @@ input[type="checkbox"] {
margin: 4px 0 0;
*margin-top: 0;
/* IE7 */
-
margin-top: 1px \9;
/* IE8-9 */
-
line-height: normal;
}
input[type="file"],
@@ -1001,10 +993,8 @@ select,
input[type="file"] {
height: 30px;
/* In IE7, the height of the select element cannot be changed by height, only font-size */
-
*margin-top: 4px;
/* For IE7, add top margin to align select with labels */
-
line-height: 30px;
}
select {
@@ -1402,7 +1392,6 @@ select:focus:invalid:focus {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
vertical-align: middle;
padding-left: 5px;
@@ -1553,7 +1542,6 @@ input.search-query {
padding-left: 14px;
padding-left: 4px \9;
/* IE7-8 doesn't have border-radius, so don't indent the padding */
-
margin-bottom: 0;
-webkit-border-radius: 15px;
-moz-border-radius: 15px;
@@ -1610,7 +1598,6 @@ input.search-query {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
margin-bottom: 0;
vertical-align: middle;
@@ -2220,7 +2207,6 @@ button.close {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
padding: 4px 12px;
margin-bottom: 0;
@@ -2243,7 +2229,6 @@ button.close {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #eaeaea;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
border: 1px solid #cccccc;
*border: 0;
@@ -2379,7 +2364,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #c14531;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-primary:hover,
@@ -2411,7 +2395,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #f89406;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-warning:hover,
@@ -2443,7 +2426,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #bd362f;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-danger:hover,
@@ -2475,7 +2457,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #51a351;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-success:hover,
@@ -2507,7 +2488,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #2f96b4;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-info:hover,
@@ -2539,7 +2519,6 @@ input[type="button"].btn-block {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #222222;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.btn-inverse:hover,
@@ -2614,7 +2593,6 @@ input[type="submit"].btn.btn-mini {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
font-size: 0;
vertical-align: middle;
@@ -2790,7 +2768,6 @@ input[type="submit"].btn.btn-mini {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
}
.btn-group-vertical > .btn {
@@ -3487,7 +3464,6 @@ input[type="submit"].btn.btn-mini {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #e5e5e5;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
-webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.075);
-moz-box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.075);
@@ -3721,7 +3697,6 @@ input[type="submit"].btn.btn-mini {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #040404;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.navbar-inverse .btn-navbar:hover,
@@ -3751,7 +3726,6 @@ input[type="submit"].btn.btn-mini {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
text-shadow: 0 1px 0 #ffffff;
}
@@ -3769,7 +3743,6 @@ input[type="submit"].btn.btn-mini {
display: inline-block;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
margin-left: 0;
margin-bottom: 0;
@@ -3970,7 +3943,6 @@ input[type="submit"].btn.btn-mini {
border: 1px solid rgba(0, 0, 0, 0.3);
*border: 1px solid #999;
/* IE6-7 */
-
-webkit-border-radius: 6px;
-moz-border-radius: 6px;
border-radius: 6px;
@@ -5575,6 +5547,12 @@ textarea {
}
.form-actions .control-required-message {
float: left;
+ margin-left: 20px;
+ margin-bottom: 0;
+ line-height: 30px;
+}
+.form-actions .control-required-message:first-child {
+ margin-left: 0;
}
.form-actions {
background: none;
@@ -5692,27 +5670,24 @@ textarea {
padding: 7px 5px;
}
.simple-input .field .btn-search {
- *margin-right: .3em;
- display: inline-block;
- vertical-align: text-bottom;
- position: relative;
- top: 2px;
- width: 16px;
- height: 16px;
- background-image: url("../../../base/images/sprite-ckan-icons.png");
- background-repeat: no-repeat;
- background-position: 16px 16px;
- background-position: -51px -16px;
position: absolute;
display: block;
height: 17px;
width: 17px;
+ padding: 0;
top: 50%;
right: 0;
- margin-top: -8px;
+ margin-top: -10px;
background-color: transparent;
border: none;
- text-indent: -999em;
+ color: #999;
+ -webkit-transition: color 0.2s ease-in;
+ -moz-transition: color 0.2s ease-in;
+ -o-transition: color 0.2s ease-in;
+ transition: color 0.2s ease-in;
+}
+.simple-input .field .btn-search:hover {
+ color: #000;
}
.editor textarea {
-webkit-border-radius: 3px 3px 0 0;
@@ -5818,7 +5793,6 @@ textarea {
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #c14531;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
-
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.control-custom.disabled .checkbox.btn:hover,
@@ -6097,7 +6071,6 @@ textarea {
outline: 0;
outline: thin dotted \9;
/* IE6-9 */
-
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
-moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
@@ -6120,6 +6093,43 @@ textarea {
-moz-box-shadow: none;
box-shadow: none;
}
+.js .image-upload #field-image-upload {
+ cursor: pointer;
+ position: absolute;
+ z-index: 1;
+ opacity: 0;
+ filter: alpha(opacity=0);
+}
+.js .image-upload .controls {
+ position: relative;
+}
+.js .image-upload .btn {
+ position: relative;
+ top: 0;
+ margin-right: 10px;
+}
+.js .image-upload .btn.hover {
+ color: #333333;
+ text-decoration: none;
+ background-position: 0 -15px;
+ -webkit-transition: background-position 0.1s linear;
+ -moz-transition: background-position 0.1s linear;
+ -o-transition: background-position 0.1s linear;
+ transition: background-position 0.1s linear;
+}
+.js .image-upload .btn-remove-url {
+ position: absolute;
+ margin-right: 0;
+ top: 4px;
+ right: 5px;
+ padding: 0 4px;
+ -webkit-border-radius: 100px;
+ -moz-border-radius: 100px;
+ border-radius: 100px;
+}
+.js .image-upload .btn-remove-url .icon-remove {
+ margin-right: 0;
+}
.add-member-form .control-label {
width: 100%;
text-align: left;
@@ -6414,6 +6424,10 @@ textarea {
font-weight: normal;
color: #000000;
}
+.search-form.no-bottom-border {
+ border-bottom-width: 0;
+ margin-bottom: 0;
+}
.tertiary .control-order-by {
float: none;
margin: 0;
@@ -6534,7 +6548,6 @@ textarea {
margin-right: 5px;
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
}
.actions li:last-of-type {
@@ -7159,6 +7172,11 @@ h4 small {
.context-info.editing .module-content {
margin-top: 0;
}
+.flash-messages .alert {
+ -webkit-box-shadow: 0 0 0 1px #ffffff;
+ -moz-box-shadow: 0 0 0 1px #ffffff;
+ box-shadow: 0 0 0 1px #ffffff;
+}
.homepage [role=main] {
padding: 20px 0;
min-height: 0;
@@ -7711,6 +7729,9 @@ h4 small {
-moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
+.activity .item.no-avatar p {
+ margin-left: 40px;
+}
.activity .load-less {
margin-bottom: 15px;
}
@@ -7728,9 +7749,28 @@ h4 small {
float: right;
text-decoration: none;
}
+.popover .popover-content {
+ font-size: 14px;
+ line-height: 20px;
+ color: #444444;
+ word-break: break-all;
+}
+.popover .popover-content dl {
+ margin: 0;
+}
+.popover .popover-content dl dd {
+ margin-left: 0;
+ margin-bottom: 10px;
+}
.activity .item .icon {
background-color: #999999;
}
+.activity .item.failure .icon {
+ background-color: #b95252;
+}
+.activity .item.success .icon {
+ background-color: #69a67a;
+}
.activity .item.added-tag .icon {
background-color: #6995a6;
}
@@ -7981,6 +8021,21 @@ h4 small {
font-size: 16px;
margin: 3px 0;
}
+.datapusher-status-link:hover {
+ text-decoration: none;
+}
+.datapusher-status.status-unknown {
+ color: #bbb;
+}
+.datapusher-status.status-pending {
+ color: #FFCC00;
+}
+.datapusher-status.status-error {
+ color: red;
+}
+.datapusher-status.status-complete {
+ color: #009900;
+}
body {
background: #c14531 url("../../../base/images/bg.png");
}
@@ -8182,7 +8237,6 @@ iframe {
.ie7 .control-custom .checkbox {
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
}
.ie7 .stages {
@@ -8216,7 +8270,6 @@ iframe {
.ie7 .masthead nav {
*display: inline;
/* IE7 inline-block hack */
-
*zoom: 1;
}
.ie7 .masthead .header-image {
diff --git a/ckan/public/base/javascript/client.js b/ckan/public/base/javascript/client.js
index 45940c0086d..e1c60ccf24e 100644
--- a/ckan/public/base/javascript/client.js
+++ b/ckan/public/base/javascript/client.js
@@ -370,7 +370,7 @@
});
ckan.sandbox.setup(function (instance) {
- instance.client = new Client({endpoint: ckan.API_ROOT});
+ instance.client = new Client({endpoint: ckan.SITE_ROOT});
});
ckan.Client = Client;
diff --git a/ckan/public/base/javascript/main.js b/ckan/public/base/javascript/main.js
index 42c76dfd15f..e3d1e42282a 100644
--- a/ckan/public/base/javascript/main.js
+++ b/ckan/public/base/javascript/main.js
@@ -29,14 +29,17 @@ this.ckan = this.ckan || {};
ckan.SITE_ROOT = getRootFromData('siteRoot');
ckan.LOCALE_ROOT = getRootFromData('localeRoot');
- ckan.API_ROOT = getRootFromData('apiRoot');
// Load the localisations before instantiating the modules.
ckan.sandbox().client.getLocaleData(locale).done(function (data) {
ckan.i18n.load(data);
ckan.module.initialize();
});
- jQuery('[data-target="popover"]').popover();
+
+ if (jQuery.fn.popover !== undefined) {
+ jQuery('[data-target="popover"]').popover();
+ }
+
};
/* Returns a full url for the current site with the provided path appended.
diff --git a/ckan/public/base/javascript/modules/image-upload.js b/ckan/public/base/javascript/modules/image-upload.js
new file mode 100644
index 00000000000..1872a051fbb
--- /dev/null
+++ b/ckan/public/base/javascript/modules/image-upload.js
@@ -0,0 +1,188 @@
+/* Image Upload
+ *
+ */
+this.ckan.module('image-upload', function($, _) {
+ return {
+ /* options object can be extended using data-module-* attributes */
+ options: {
+ is_url: true,
+ is_upload: false,
+ field_upload: 'image_upload',
+ field_url: 'image_url',
+ field_clear: 'clear_upload',
+ upload_label: '',
+ i18n: {
+ upload: _('From computer'),
+ url: _('From web'),
+ remove: _('Remove'),
+ upload_label: _('Upload image'),
+ remove_tooltip: _('Reset this')
+ },
+ template: [
+ ''
+ ].join("\n")
+ },
+
+ state: {
+ attached: 1,
+ blank: 2,
+ web: 3
+ },
+
+ /* Initialises the module setting up elements and event listeners.
+ *
+ * Returns nothing.
+ */
+ initialize: function () {
+ $.proxyAll(this, /_on/);
+ var options = this.options;
+
+ // firstly setup the fields
+ var field_upload = 'input[name="' + options.field_upload + '"]';
+ var field_url = 'input[name="' + options.field_url + '"]';
+ var field_clear = 'input[name="' + options.field_clear + '"]';
+
+ this.input = $(field_upload, this.el);
+ this.field_url = $(field_url, this.el).parents('.control-group');
+ this.field_image = this.input.parents('.control-group');
+
+ // Is there a clear checkbox on the form already?
+ var checkbox = $(field_clear, this.el);
+ if (checkbox.length > 0) {
+ options.is_upload = true;
+ checkbox.parents('.control-group').remove();
+ }
+
+ // Adds the hidden clear input to the form
+ this.field_clear = $('')
+ .appendTo(this.el);
+
+ // Button to set the field to be a URL
+ this.button_url = $(' '+this.i18n('url')+'')
+ .on('click', this._onFromWeb)
+ .insertAfter(this.input);
+
+ // Button to attach local file to the form
+ this.button_upload = $(''+this.i18n('upload')+'')
+ .insertAfter(this.input);
+
+ // Button to reset the form back to the first from when there is a image uploaded
+ this.button_remove = $('')
+ .text(this.i18n('remove'))
+ .on('click', this._onRemove)
+ .insertAfter(this.button_upload);
+
+ // Button for resetting the form when there is a URL set
+ $('')
+ .prop('title', this.i18n('remove_tooltip'))
+ .on('click', this._onRemove)
+ .insertBefore($('input', this.field_url));
+
+ // Update the main label
+ $('label[for="field-image-upload"]').text(options.upload_label || this.i18n('upload_label'));
+
+ // Setup the file input
+ this.input
+ .on('mouseover', this._onInputMouseOver)
+ .on('mouseout', this._onInputMouseOut)
+ .on('change', this._onInputChange)
+ .css('width', this.button_upload.outerWidth());
+
+ // Fields storage. Used in this.changeState
+ this.fields = $('')
+ .add(this.button_remove)
+ .add(this.button_upload)
+ .add(this.button_url)
+ .add(this.input)
+ .add(this.field_url)
+ .add(this.field_image);
+
+ // Setup the initial state
+ if (options.is_url) {
+ this.changeState(this.state.web);
+ } else if (options.is_upload) {
+ this.changeState(this.state.attached);
+ } else {
+ this.changeState(this.state.blank);
+ }
+
+ },
+
+ /* Method to change the display state of the image fields
+ *
+ * state - Pseudo constant for passing the state we should be in now
+ *
+ * Examples
+ *
+ * this.changeState(this.state.web); // Sets the state in URL mode
+ *
+ * Returns nothing.
+ */
+ changeState: function(state) {
+ this.fields.hide();
+ if (state == this.state.blank) {
+ this.button_upload
+ .add(this.field_image)
+ .add(this.button_url)
+ .add(this.input)
+ .show();
+ } else if (state == this.state.attached) {
+ this.button_remove
+ .add(this.field_image)
+ .show();
+ } else if (state == this.state.web) {
+ this.field_url
+ .show();
+ }
+ },
+
+ /* Event listener for when someone sets the field to URL mode
+ *
+ * Returns nothing.
+ */
+ _onFromWeb: function() {
+ this.changeState(this.state.web);
+ $('input', this.field_url).focus();
+ if (this.options.is_upload) {
+ this.field_clear.val('true');
+ }
+ },
+
+ /* Event listener for resetting the field back to the blank state
+ *
+ * Returns nothing.
+ */
+ _onRemove: function() {
+ this.changeState(this.state.blank);
+ $('input', this.field_url).val('');
+ this.field_clear.val('true');
+ },
+
+ /* Event listener for when someone chooses a file to upload
+ *
+ * Returns nothing.
+ */
+ _onInputChange: function() {
+ this.file_name = this.input.val();
+ this.field_clear.val('');
+ this.changeState(this.state.attached);
+ },
+
+ /* Event listener for when a user mouseovers the hidden file input
+ *
+ * Returns nothing.
+ */
+ _onInputMouseOver: function() {
+ this.button_upload.addClass('hover');
+ },
+
+ /* Event listener for when a user mouseouts the hidden file input
+ *
+ * Returns nothing.
+ */
+ _onInputMouseOut: function() {
+ this.button_upload.removeClass('hover');
+ }
+
+ }
+});
diff --git a/ckan/public/base/javascript/modules/slug-preview.js b/ckan/public/base/javascript/modules/slug-preview.js
index e622401ee09..63fb08d94e7 100644
--- a/ckan/public/base/javascript/modules/slug-preview.js
+++ b/ckan/public/base/javascript/modules/slug-preview.js
@@ -9,17 +9,20 @@ this.ckan.module('slug-preview-target', {
el.after(preview);
});
- // Once the preview box is modified stop watching it.
- sandbox.subscribe('slug-preview-modified', function () {
- el.off('.slug-preview');
- });
+ // Make sure there isn't a value in the field already...
+ if (el.val() == '') {
+ // Once the preview box is modified stop watching it.
+ sandbox.subscribe('slug-preview-modified', function () {
+ el.off('.slug-preview');
+ });
- // Watch for updates to the target field and update the hidden slug field
- // triggering the "change" event manually.
- el.on('keyup.slug-preview', function (event) {
- sandbox.publish('slug-target-changed', this.value);
- //slug.val(this.value).trigger('change');
- });
+ // Watch for updates to the target field and update the hidden slug field
+ // triggering the "change" event manually.
+ el.on('keyup.slug-preview', function (event) {
+ sandbox.publish('slug-target-changed', this.value);
+ //slug.val(this.value).trigger('change');
+ });
+ }
}
});
diff --git a/ckan/public/base/javascript/resource.config b/ckan/public/base/javascript/resource.config
index b1a7302d9e8..564da811a6e 100644
--- a/ckan/public/base/javascript/resource.config
+++ b/ckan/public/base/javascript/resource.config
@@ -38,6 +38,7 @@ ckan =
modules/table-toggle-more.js
modules/dataset-visibility.js
modules/media-grid.js
+ modules/image-upload.js
main =
apply_html_class
diff --git a/ckan/public/base/less/dataset.less b/ckan/public/base/less/dataset.less
index 29b07d6b8a1..a102e3040a6 100644
--- a/ckan/public/base/less/dataset.less
+++ b/ckan/public/base/less/dataset.less
@@ -95,44 +95,6 @@
// Dataset Forms
-.dataset-resource-form .dataset-form-resource-types {
- margin-bottom: 5px;
-}
-
-.dataset-form-resource-types .ckan-icon {
- position: relative;
- top: 3px;
-}
-
-.dataset-form-resource-types .radio {
- font-weight: normal;
- padding-left: 0;
- padding-right: 18px;
-}
-
-.dataset-form-resource-types label {
- position: relative;
-}
-
-.dataset-form-resource-types input[type=radio]:checked+label {
- font-weight: bold;
-}
-
-.dataset-form-resource-types input[type=radio]:checked+label:after {
- .ckan-icon;
- .ckan-icon-callout;
- display: block;
- content: "";
- position: absolute;
- top: auto;
- left: 0;
- bottom: -12px;
-}
-
-.dataset-form-resource-types input[type=radio] {
- display: none;
-}
-
// Tag List
.tag-list {
diff --git a/ckan/public/base/less/forms.less b/ckan/public/base/less/forms.less
index fc8c7c86987..bf45350340f 100644
--- a/ckan/public/base/less/forms.less
+++ b/ckan/public/base/less/forms.less
@@ -85,6 +85,12 @@ textarea {
.form-actions .control-required-message {
float: left;
+ margin-left: 20px;
+ margin-bottom: 0;
+ line-height: 30px;
+ &:first-child {
+ margin-left: 0;
+ }
}
.form-actions {
@@ -237,18 +243,21 @@ textarea {
}
.simple-input .field .btn-search {
- .ckan-icon;
- .ckan-icon-search;
position: absolute;
display: block;
height: 17px;
width: 17px;
+ padding: 0;
top: 50%;
right: 0;
- margin-top: -8px;
+ margin-top: -10px;
background-color: transparent;
border: none;
- text-indent: -999em;
+ color: #999;
+ .transition(color 0.2s ease-in);
+ &:hover {
+ color: #000;
+ }
}
.editor textarea {
@@ -671,6 +680,39 @@ textarea {
}
}
+.js .image-upload {
+ #field-image-upload {
+ cursor: pointer;
+ position: absolute;
+ z-index: 1;
+ .opacity(0);
+ }
+ .controls {
+ position: relative;
+ }
+ .btn {
+ position: relative;
+ top: 0;
+ margin-right: 10px;
+ &.hover {
+ color: @grayDark;
+ text-decoration: none;
+ background-position: 0 -15px;
+ .transition(background-position .1s linear);
+ }
+ }
+ .btn-remove-url {
+ position: absolute;
+ margin-right: 0;
+ top: 4px;
+ right: 5px;
+ padding: 0 4px;
+ .border-radius(100px);
+ .icon-remove {
+ margin-right: 0;
+ }
+ }
+}
.add-member-form .control-label {
width: 100%;
text-align: left;
diff --git a/ckan/public/base/less/layout.less b/ckan/public/base/less/layout.less
index 78905c89eee..0a74dfbe311 100644
--- a/ckan/public/base/less/layout.less
+++ b/ckan/public/base/less/layout.less
@@ -165,3 +165,10 @@
}
}
}
+
+.flash-messages {
+ .alert {
+ .box-shadow(0 0 0 1px white);
+ }
+}
+
diff --git a/ckan/public/base/less/search.less b/ckan/public/base/less/search.less
index a9e4193eb83..fffe049ff29 100644
--- a/ckan/public/base/less/search.less
+++ b/ckan/public/base/less/search.less
@@ -84,6 +84,10 @@
color: @layoutBoldColor;
}
}
+ &.no-bottom-border {
+ border-bottom-width: 0;
+ margin-bottom: 0;
+ }
}
.tertiary {
diff --git a/ckan/templates/admin/config.html b/ckan/templates/admin/config.html
index bfa9b3a2ee5..81a322b3997 100644
--- a/ckan/templates/admin/config.html
+++ b/ckan/templates/admin/config.html
@@ -10,7 +10,7 @@
{% set locale = h.dump_json({'content': _('Are you sure you want to reset the config?')}) %}
{{ _('Reset') }}
-
+
{% endblock %}
diff --git a/ckan/templates/base.html b/ckan/templates/base.html
index 6012bf65928..9ab7a69d973 100644
--- a/ckan/templates/base.html
+++ b/ckan/templates/base.html
@@ -85,7 +85,7 @@
{# Allows custom attributes to be added to the tag #}
-
+
{#
The page block allows you to add content to the page. Most of the time it is
@@ -102,17 +102,9 @@
{%- block page %}{% endblock -%}
{#
- The scripts block allows you to add additonal scripts to the page. Use the
- super() function to load the default scripts before/after your own.
- NOTE: Scripts should be loaded by the {% resource %} tag except
- in very special circumstances DO NOT USE THIS METHOD FOR ADDING SCRIPTS.
-
- Example:
-
- {% block scripts %}
- {{ super() }}
-
- {% endblock %}
+ DO NOT USE THIS BLOCK FOR ADDING SCRIPTS
+ Scripts should be loaded by the {% resource %} tag except in very special
+ circumstances
#}
{%- block scripts %}
{% endblock -%}
diff --git a/ckan/templates/group/about.html b/ckan/templates/group/about.html
index 69259e22ec3..a7d3683700d 100644
--- a/ckan/templates/group/about.html
+++ b/ckan/templates/group/about.html
@@ -1,6 +1,6 @@
{% extends "group/read_base.html" %}
-{% block subtitle %}{{ _('About') }} - {{ c.group_dict.display_name }}{% endblock %}
+{% block subtitle %}{{ _('About') }} - {{ super() }}{% endblock %}
{% block primary_content_inner %}
{{ _('There are currently no groups for this site') }}.
diff --git a/ckan/templates/group/member_new.html b/ckan/templates/group/member_new.html
index 6a8fa691c29..bead208c5ec 100644
--- a/ckan/templates/group/member_new.html
+++ b/ckan/templates/group/member_new.html
@@ -85,12 +85,9 @@
{% trans %}
-
Admin: Can add/edit and delete datasets, as well as
- manage group members.
-
Editor: Can add and edit datasets, but not manage
- group members.
-
Member: Can view the group's private
- datasets, but not add new datasets.
+
Admin: Can edit group information, as well as
+ manage organization members.