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 %}

{% block page_heading %}{{ c.group_dict.display_name }}{% endblock %}

diff --git a/ckan/templates/group/activity_stream.html b/ckan/templates/group/activity_stream.html index 800ed24db9b..47a6d029bb4 100644 --- a/ckan/templates/group/activity_stream.html +++ b/ckan/templates/group/activity_stream.html @@ -1,6 +1,6 @@ {% extends "group/read_base.html" %} -{% block subtitle %}{{ _('Activity Stream') }} - {{ c.group_dict.title or c.group_dict.name }}{% endblock %} +{% block subtitle %}{{ _('Activity Stream') }} - {{ super() }}{% endblock %} {% block primary_content_inner %}

{% block page_heading %}{{ _('Activity Stream') }}{% endblock %}

diff --git a/ckan/templates/group/edit.html b/ckan/templates/group/edit.html index 57808056d54..ff5c0b96989 100644 --- a/ckan/templates/group/edit.html +++ b/ckan/templates/group/edit.html @@ -4,11 +4,9 @@
  • {% link_for _('Groups'), controller='group', action='index' %}
  • {% block breadcrumb_content_inner %}
  • {% link_for group.display_name|truncate(35), controller='group', action='read', id=group.name %}
  • -
  • {% link_for _('Edit'), controller='group', action='edit', id=group.name %}
  • +
  • {% link_for _('Manage'), controller='group', action='edit', id=group.name %}
  • {% endblock %} {% endblock %} -{% block subtitle %}{{ _('Edit Group') }}{% endblock %} - {% block page_heading_class %}hide-heading{% endblock %} {% block page_heading %}{{ _('Edit Group') }}{% endblock %} diff --git a/ckan/templates/group/edit_base.html b/ckan/templates/group/edit_base.html index a5f1203d529..566da231192 100644 --- a/ckan/templates/group/edit_base.html +++ b/ckan/templates/group/edit_base.html @@ -1,17 +1,19 @@ {% extends "page.html" %} +{% block subtitle %}{{ _('Manage') }} - {{ c.group_dict.display_name }} - {{ _('Groups') }}{% endblock %} + {% set group = c.group_dict %} {% block breadcrumb_content %}
  • {% link_for _('Groups'), controller='group', action='index' %}
  • {% block breadcrumb_content_inner %}
  • {% link_for group.display_name|truncate(35), controller='group', action='read', id=group.name %}
  • -
  • {% link_for _('Edit'), controller='group', action='edit', id=group.name %}
  • +
  • {% link_for _('Manage'), controller='group', action='edit', id=group.name %}
  • {% endblock %} {% endblock %} {% block content_action %} - {% link_for _('View group'), controller='group', action='read', id=c.group_dict.name, class_='btn', icon='eye-open' %} + {% link_for _('View'), controller='group', action='read', id=c.group_dict.name, class_='btn', icon='eye-open' %} {% endblock %} {% block content_primary_nav %} diff --git a/ckan/templates/group/index.html b/ckan/templates/group/index.html index 6298cb5a120..514c12bb961 100644 --- a/ckan/templates/group/index.html +++ b/ckan/templates/group/index.html @@ -1,6 +1,6 @@ {% extends "page.html" %} -{% block subtitle %}{{ _('Groups of Datasets') }}{% endblock %} +{% block subtitle %}{{ _('Groups') }}{% endblock %} {% block breadcrumb_content %}
  • {% link_for _('Groups'), controller='group', action='index' %}
  • @@ -17,11 +17,13 @@ {% block primary_content_inner %}

    {{ _('Groups') }}

    {% block groups_search_form %} - {% snippet 'snippets/search_form.html', type='group', query=c.q, sorting_selected=c.sort_by_selected, count=c.page.item_count, placeholder=_('Search groups...'), show_empty=request.params %} + {% snippet 'snippets/search_form.html', type='group', query=c.q, sorting_selected=c.sort_by_selected, count=c.page.item_count, placeholder=_('Search groups...'), show_empty=request.params, no_bottom_border=true if c.page.items %} {% endblock %} {% block groups_list %} {% if c.page.items or request.params %} - {% snippet "group/snippets/group_list.html", groups=c.page.items %} + {% if c.page.items %} + {% snippet "group/snippets/group_list.html", groups=c.page.items %} + {% endif %} {% else %}

    {{ _('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.

    +

    Member: Can add/remove datasets from groups

    {% endtrans %}
    diff --git a/ckan/templates/group/members.html b/ckan/templates/group/members.html index 600c6d1bf54..c758972d54b 100644 --- a/ckan/templates/group/members.html +++ b/ckan/templates/group/members.html @@ -1,6 +1,6 @@ {% extends "group/edit_base.html" %} -{% block subtitle %}{{ _('Members') }} - {{ c.group_dict.display_name }}{% endblock %} +{% block subtitle %}{{ _('Members') }} - {{ c.group_dict.display_name }} - {{ _('Groups') }}{% endblock %} {% block primary_content_inner %} {% link_for _('Add Member'), controller='group', action='member_new', id=c.group_dict.id, class_='btn pull-right', icon='plus-sign-alt' %} diff --git a/ckan/templates/group/new.html b/ckan/templates/group/new.html index 23d66facb05..1d78ac40010 100644 --- a/ckan/templates/group/new.html +++ b/ckan/templates/group/new.html @@ -2,7 +2,7 @@ {% block subtitle %}{{ _('Create a Group') }}{% endblock %} -{% block breadcrumb_link %}{{ h.nav_link(_('Create Group'), controller='group', action='edit', id=c.group.name) }}{% endblock %} +{% block breadcrumb_link %}{{ h.nav_link(_('Create a Group'), controller='group', action='edit', id=c.group.name) }}{% endblock %} {% block page_heading %}{{ _('Create a Group') }}{% endblock %} diff --git a/ckan/templates/group/read.html b/ckan/templates/group/read.html index 8218edb4b37..81b967e53d7 100644 --- a/ckan/templates/group/read.html +++ b/ckan/templates/group/read.html @@ -2,10 +2,6 @@ {% block subtitle %}{{ c.group_dict.display_name }}{% endblock %} -{% block page_primary_action %} - {% link_for _('Add Dataset'), controller='package', action='new', group=c.group_dict.id, class_='btn btn-primary', icon='plus-sign-alt' %} -{% endblock %} - {% block primary_content_inner %} {% block groups_search_form %} {% set facets = { diff --git a/ckan/templates/group/read_base.html b/ckan/templates/group/read_base.html index afa92f2038f..9164f28ed64 100644 --- a/ckan/templates/group/read_base.html +++ b/ckan/templates/group/read_base.html @@ -1,6 +1,6 @@ {% extends "page.html" %} -{% block subtitle %}{{ c.group_dict.display_name }}{% endblock %} +{% block subtitle %}{{ c.group_dict.display_name }} - {{ _('Groups') }}{% endblock %} {% block breadcrumb_content %}
  • {% link_for _('Groups'), controller='group', action='index' %}
  • @@ -9,7 +9,7 @@ {% block content_action %} {% if h.check_access('group_update', {'id': c.group_dict.id}) %} - {% link_for _('Edit'), controller='group', action='edit', id=c.group_dict.name, class_='btn', icon='wrench' %} + {% link_for _('Manage'), controller='group', action='edit', id=c.group_dict.name, class_='btn', icon='wrench' %} {% endif %} {% endblock %} diff --git a/ckan/templates/group/snippets/group_form.html b/ckan/templates/group/snippets/group_form.html index 6d4d3216b26..a6d5fbcbf95 100644 --- a/ckan/templates/group/snippets/group_form.html +++ b/ckan/templates/group/snippets/group_form.html @@ -1,6 +1,6 @@ {% import 'macros/form.html' as form %} -
    + {% block error_summary %} {{ form.errors(error_summary) }} {% endblock %} @@ -19,7 +19,10 @@ {{ form.markdown('description', label=_('Description'), id='field-description', placeholder=_('A little information about my group...'), value=data.description, error=errors.description) }} - {{ form.input('image_url', label=_('Image URL'), id='field-image-url', type='url', placeholder=_('http://example.com/my-image.jpg'), value=data.image_url, error=errors.image_url, classes=['control-full']) }} + {% set is_upload = data.image_url and not data.image_url.startswith('http') %} + {% set is_url = data.image_url and data.image_url.startswith('http') %} + + {{ form.image_upload(data, errors, is_upload_enabled=h.uploads_enabled(), is_url=is_url, is_upload=is_upload) }} {% endblock %} @@ -48,6 +51,8 @@ ) }} {% endfor %} {% endblock %} + + {{ form.required_message() }} {# Do not update datasets here {% block dataset_fields %} {% if data.packages %} @@ -70,13 +75,13 @@ #}
    + {% block delete_button %} + {% if h.check_access('group_delete', {'id': data.id}) %} + {% set locale = h.dump_json({'content': _('Are you sure you want to delete this Group?')}) %} + {% block delete_button_text %}{{ _('Delete') }}{% endblock %} + {% endif %} + {% endblock %} {{ form.required_message() }} - {% block delete_button %} - {% if h.check_access('group_delete', {'id': data.id}) %} - {% set locale = h.dump_json({'content': _('Are you sure you want to delete this Group?')}) %} - {% block delete_button_text %}{{ _('Delete') }}{% endblock %} - {% endif %} - {% endblock %}
    diff --git a/ckan/templates/group/snippets/group_item.html b/ckan/templates/group/snippets/group_item.html index 9e32ce86e6a..04351329f2b 100644 --- a/ckan/templates/group/snippets/group_item.html +++ b/ckan/templates/group/snippets/group_item.html @@ -11,10 +11,11 @@ {% endfor %} #} -{% set url = h.url_for(group.type ~ '_read', action='read', id=group.name) %} +{% set type = group.type or 'group' %} +{% set url = h.url_for(type ~ '_read', action='read', id=group.name) %}
  • {% block image %} - {{ group.name }} + {{ group.name }} {% endblock %} {% block title %}

    {{ group.display_name }}

    @@ -29,13 +30,16 @@

    {{ group.display_name }}

    {% block datasets %} {% if group.packages %} {{ ungettext('{num} Dataset', '{num} Datasets', group.packages).format(num=group.packages) }} - {% else %} + {% elif group.packages == 0 %} {{ _('0 Datasets') }} {% endif %} {% endblock %} {{ _('View {name}').format(name=group.display_name) }} + {% if group.user_member %} + + {% endif %}
  • {% if position is divisibleby 3 %}
  • diff --git a/ckan/templates/group/snippets/info.html b/ckan/templates/group/snippets/info.html index b7c749ab147..dc86036bb4c 100644 --- a/ckan/templates/group/snippets/info.html +++ b/ckan/templates/group/snippets/info.html @@ -2,7 +2,7 @@

    {{ group.display_name }}

    diff --git a/ckan/templates/header.html b/ckan/templates/header.html index e8aa9321098..dd0d5c07c57 100644 --- a/ckan/templates/header.html +++ b/ckan/templates/header.html @@ -50,7 +50,9 @@
      {% block header_account_notlogged %}
    • {% link_for _('Log in'), controller='user', action='login' %}
    • -
    • {% link_for _('Register'), controller='user', action='register', class_='sub' %}
    • + {% if h.check_access('user_create') %} +
    • {% link_for _('Register'), controller='user', action='register', class_='sub' %}
    • + {% endif %} {% endblock %}
    @@ -103,8 +105,8 @@

    {% endblock %} diff --git a/ckan/templates/macros/form.html b/ckan/templates/macros/form.html index 497dea6bf02..0c0062c873b 100644 --- a/ckan/templates/macros/form.html +++ b/ckan/templates/macros/form.html @@ -395,3 +395,35 @@

    {% endmacro %} +{# +Builds a file upload for input + +Example + {% import 'macros/form.html' as form %} + {{ form.image_upload(data, errors, is_upload_enabled=true) }} + +#} +{% macro image_upload(data, errors, field_url='image_url', field_upload='image_upload', field_clear='clear_upload', + is_url=false, is_upload=false, is_upload_enabled=false, placeholder=false, + url_label='', upload_label='') %} + {% set placeholder = placeholder if placeholder else _('http://example.com/my-image.jpg') %} + {% set url_label = url_label or _('Image URL') %} + {% set upload_label = upload_label or _('Upload Image') %} + + {% if is_upload_enabled %} +
    + {% endif %} + + {{ input(field_url, label=url_label, id='field-image-url', placeholder=placeholder, value=data.get(field_url), error=errors.get(field_url), classes=['control-full']) }} + + {% if is_upload_enabled %} + {{ input(field_upload, label=upload_label, id='field-image-upload', type='file', placeholder='', value='', error='', classes=['control-full']) }} + {% if is_upload %} + {{ checkbox(field_clear, label=_('Clear Upload'), id='field-clear-upload', value='true', error='', classes=['control-full']) }} + {% endif %} + {% endif %} + + {% if is_upload_enabled %}
    {% endif %} + +{% endmacro %} diff --git a/ckan/templates/organization/about.html b/ckan/templates/organization/about.html index 6848ed9ede7..6d3fcfe1bfd 100644 --- a/ckan/templates/organization/about.html +++ b/ckan/templates/organization/about.html @@ -1,6 +1,6 @@ {% extends "organization/read_base.html" %} -{% block subtitle %}{{ _('About') }} - {{ c.group_dict.display_name }}{% endblock %} +{% block subtitle %}{{ _('About') }} - {{ super() }}{% endblock %} {% block primary_content_inner %}

    {% block page_heading %}{{ c.group_dict.display_name }}{% endblock %}

    diff --git a/ckan/templates/organization/activity_stream.html b/ckan/templates/organization/activity_stream.html index dc8e711c5c8..ffa029d951b 100644 --- a/ckan/templates/organization/activity_stream.html +++ b/ckan/templates/organization/activity_stream.html @@ -1,6 +1,6 @@ {% extends "organization/read_base.html" %} -{% block subtitle %}{{ _('Activity Stream') }} - {{ c.group_dict.title or c.group_dict.name }}{% endblock %} +{% block subtitle %}{{ _('Activity Stream') }} - {{ super() }}{% endblock %} {% block primary_content_inner %}

    {% block page_heading %}{{ _('Activity Stream') }}{% endblock %}

    diff --git a/ckan/templates/organization/edit.html b/ckan/templates/organization/edit.html index 06f7c7ee8ed..3fde80a2ab0 100644 --- a/ckan/templates/organization/edit.html +++ b/ckan/templates/organization/edit.html @@ -1,6 +1,6 @@ {% extends "organization/base_form_page.html" %} -{% block subtitle %}{{ _('Edit Organization') }}{% endblock %} +{% block subtitle %}{{ _('Edit') }} - {{ super() }}{% endblock %} {% block page_heading_class %}hide-heading{% endblock %} {% block page_heading %}{{ _('Edit Organization') }}{% endblock %} diff --git a/ckan/templates/organization/edit_base.html b/ckan/templates/organization/edit_base.html index 6f29a82b1f8..daaeef556e5 100644 --- a/ckan/templates/organization/edit_base.html +++ b/ckan/templates/organization/edit_base.html @@ -2,21 +2,20 @@ {% set organization = c.group_dict %} -{% block subtitle %}{{ organization.display_name }}{% endblock %} +{% block subtitle %}{{ c.group_dict.display_name }} - {{ _('Organizations') }}{% endblock %} {% block breadcrumb_content %}
  • {% link_for _('Organizations'), controller='organization', action='index' %}
  • {% block breadcrumb_content_inner %}
  • {% link_for organization.display_name|truncate(35), controller='organization', action='read', id=organization.name %}
  • -
  • {% link_for _('Admin'), controller='organization', action='edit', id=organization.name %}
  • +
  • {% link_for _('Manage'), controller='organization', action='edit', id=organization.name %}
  • {% endblock %} {% endblock %} {% block content_action %} {% if organization and h.check_access('organization_update', {'id': organization.id}) %} - {% link_for _('View organization'), controller='organization', action='read', id=organization.name, class_='btn', icon='eye-open' %} + {% link_for _('View'), controller='organization', action='read', id=organization.name, class_='btn', icon='eye-open' %} {% endif %} - {#
  • {% link_for _('History'), controller='organization', action='history', id=organization.name, class_='btn', icon='undo' %}
  • #} {% endblock %} {% block content_primary_nav %} diff --git a/ckan/templates/organization/index.html b/ckan/templates/organization/index.html index ae2fb2bc49b..3b317ec701c 100644 --- a/ckan/templates/organization/index.html +++ b/ckan/templates/organization/index.html @@ -17,11 +17,13 @@ {% block primary_content_inner %}

    {% block page_heading %}{{ _('Organizations') }}{% endblock %}

    {% block organizations_search_form %} - {% snippet 'snippets/search_form.html', type='organization', query=c.q, sorting_selected=c.sort_by_selected, count=c.page.item_count, placeholder=_('Search organizations...'), show_empty=request.params %} + {% snippet 'snippets/search_form.html', type='organization', query=c.q, sorting_selected=c.sort_by_selected, count=c.page.item_count, placeholder=_('Search organizations...'), show_empty=request.params, no_bottom_border=true if c.page.items %} {% endblock %} {% block organizations_list %} {% if c.page.items or request.params %} - {% snippet "organization/snippets/organization_list.html", organizations=c.page.items %} + {% if c.page.items %} + {% snippet "organization/snippets/organization_list.html", organizations=c.page.items %} + {% endif %} {% else %}

    {{ _('There are currently no organizations for this site') }}. diff --git a/ckan/templates/organization/member_new.html b/ckan/templates/organization/member_new.html index a121549dab4..cc4ac8c7dd4 100644 --- a/ckan/templates/organization/member_new.html +++ b/ckan/templates/organization/member_new.html @@ -4,7 +4,7 @@ {% set user = c.user_dict %} -{% block subtitle %}{{ _('Members') }} - {{ c.group_dict.display_name }}{% endblock %} +{% block subtitle %}{{ _('Edit Member') if user else _('Add Member') }} - {{ super() }}{% endblock %} {% block primary_content_inner %} {% link_for _('Back to all members'), controller='organization', action='members', id=organization.name, class_='btn pull-right', icon='arrow-left' %} @@ -55,11 +55,11 @@

    {% set locale = h.dump_json({'content': _('Are you sure you want to delete this member?')}) %} {{ _('Delete') }} {% else %} {% endif %} diff --git a/ckan/templates/organization/members.html b/ckan/templates/organization/members.html index c5dcd0ebdae..6e3f4446b9f 100644 --- a/ckan/templates/organization/members.html +++ b/ckan/templates/organization/members.html @@ -1,6 +1,6 @@ {% extends "organization/edit_base.html" %} -{% block subtitle %}{{ _('Members') }} - {{ c.group_dict.display_name }}{% endblock %} +{% block subtitle %}{{ _('Members') }} - {{ super() }}{% endblock %} {% block page_primary_action %} {% link_for _('Add Member'), controller='organization', action='member_new', id=c.group_dict.id, class_='btn btn-primary', icon='plus-sign-alt' %} diff --git a/ckan/templates/organization/read_base.html b/ckan/templates/organization/read_base.html index d5a0f8120d5..1c9454f4729 100644 --- a/ckan/templates/organization/read_base.html +++ b/ckan/templates/organization/read_base.html @@ -1,6 +1,6 @@ {% extends "page.html" %} -{% block subtitle %}{{ c.group_dict.display_name }}{% endblock %} +{% block subtitle %}{{ c.group_dict.display_name }} - {{ _('Organizations') }}{% endblock %} {% block breadcrumb_content %}
  • {% link_for _('Organizations'), controller='organization', action='index' %}
  • @@ -9,7 +9,7 @@ {% block content_action %} {% if h.check_access('organization_update', {'id': c.group_dict.id}) %} - {% link_for _('Admin'), controller='organization', action='edit', id=c.group_dict.name, class_='btn', icon='wrench' %} + {% link_for _('Manage'), controller='organization', action='edit', id=c.group_dict.name, class_='btn', icon='wrench' %} {% endif %} {% endblock %} diff --git a/ckan/templates/organization/snippets/organization_form.html b/ckan/templates/organization/snippets/organization_form.html index 7b05e7d1542..f98cb3d9b09 100644 --- a/ckan/templates/organization/snippets/organization_form.html +++ b/ckan/templates/organization/snippets/organization_form.html @@ -1,6 +1,6 @@ {% import 'macros/form.html' as form %} -
    + {% block error_summary %} {{ form.errors(error_summary) }} {% endblock %} @@ -19,7 +19,10 @@ {{ form.markdown('description', label=_('Description'), id='field-description', placeholder=_('A little information about my organization...'), value=data.description, error=errors.description) }} - {{ form.input('image_url', label=_('Image URL'), id='field-image-url', type='url', placeholder=_('http://example.com/my-image.jpg'), value=data.image_url, error=errors.image_url, classes=['control-full']) }} + {% set is_upload = data.image_url and not data.image_url.startswith('http') %} + {% set is_url = data.image_url and data.image_url.startswith('http') %} + + {{ form.image_upload(data, errors, is_upload_enabled=h.uploads_enabled(), is_url=is_url, is_upload=is_upload) }} {% endblock %} @@ -71,14 +74,14 @@ {% endblock %} #} -
    {{ form.required_message() }} - {% block delete_button %} - {% if h.check_access('organization_delete', {'id': data.id}) %} - {% set locale = h.dump_json({'content': _('Are you sure you want to delete this Organization? This will delete all the public and private datasets belonging to this organization.')}) %} - {% block delete_button_text %}{{ _('Delete') }}{% endblock %} - {% endif %} - {% endblock %} +
    + {% block delete_button %} + {% if h.check_access('organization_delete', {'id': data.id}) %} + {% set locale = h.dump_json({'content': _('Are you sure you want to delete this Organization? This will delete all the public and private datasets belonging to this organization.')}) %} + {% block delete_button_text %}{{ _('Delete') }}{% endblock %} + {% endif %} + {% endblock %}
    diff --git a/ckan/templates/organization/snippets/organization_item.html b/ckan/templates/organization/snippets/organization_item.html index 818d5cc338a..5d774cb7f39 100644 --- a/ckan/templates/organization/snippets/organization_item.html +++ b/ckan/templates/organization/snippets/organization_item.html @@ -14,7 +14,7 @@ {% set url = h.url_for(organization.type ~ '_read', action='read', id=organization.name) %}
  • {% block image %} - {{ organization.name }} + {{ organization.name }} {% endblock %} {% block title %}

    {{ organization.display_name }}

    diff --git a/ckan/templates/package/activity.html b/ckan/templates/package/activity.html index 52c0f8eade7..3941a6dda02 100644 --- a/ckan/templates/package/activity.html +++ b/ckan/templates/package/activity.html @@ -1,6 +1,6 @@ {% extends "package/read_base.html" %} -{% block subtitle %}{{ _('Activity Stream') }} - {{ c.pkg_dict.title or c.pkg_dict.name }}{% endblock %} +{% block subtitle %}{{ _('Activity Stream') }} - {{ super() }}{% endblock %} {% block primary_content_inner %}

    {% block page_heading %}{{ _('Activity Stream') }}{% endblock %}

    diff --git a/ckan/templates/package/activity_stream.html b/ckan/templates/package/activity_stream.html deleted file mode 100644 index af67b063a7b..00000000000 --- a/ckan/templates/package/activity_stream.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "package/read.html" %} - -{% block subtitle %}{{ _('Activity Stream') }}{% endblock %} - -{% block package_content %} -

    {% block page_heading %}{{ _('Activity Stream') }}{% endblock %}

    - {% block activity_stream %} - {{ c.package_activity_stream | safe }} - {% endblock %} -{% endblock %} diff --git a/ckan/templates/package/base.html b/ckan/templates/package/base.html index b51f78faaa6..20dd0b1db27 100644 --- a/ckan/templates/package/base.html +++ b/ckan/templates/package/base.html @@ -4,6 +4,8 @@ {% block breadcrumb_content_selected %} class="active"{% endblock %} +{% block subtitle %}{{ _('Datasets') }}{% endblock %} + {% block breadcrumb_content %} {% if pkg %} {% set dataset = pkg.title or pkg.name %} diff --git a/ckan/templates/package/base_form_page.html b/ckan/templates/package/base_form_page.html index b4d7dc440c0..832fbbb2554 100644 --- a/ckan/templates/package/base_form_page.html +++ b/ckan/templates/package/base_form_page.html @@ -1,4 +1,4 @@ -{% extends "package/base.html" %} +{% extends "package/edit_base.html" %} {% block primary_content %}
    diff --git a/ckan/templates/package/group_list.html b/ckan/templates/package/group_list.html new file mode 100644 index 00000000000..34b29496c11 --- /dev/null +++ b/ckan/templates/package/group_list.html @@ -0,0 +1,26 @@ +{% extends "package/read_base.html" %} +{% import 'macros/form.html' as form %} + +{% block primary_content_inner %} +

    {{ _('Groups') }}

    + + {% if c.group_dropdown %} +
    + + +
    + {% endif %} + + {% if c.pkg_dict.groups %} +
    + {% snippet 'group/snippets/group_list.html', groups=c.pkg_dict.groups %} +
    + {% else %} +

    {{ _('There are no groups associated with this dataset') }}

    + {% endif %} + +{% endblock %} diff --git a/ckan/templates/package/new.html b/ckan/templates/package/new.html index a3e7bae8e1c..a8eea92ca48 100644 --- a/ckan/templates/package/new.html +++ b/ckan/templates/package/new.html @@ -1,3 +1,3 @@ {% extends "package/base_form_page.html" %} -{% block subtitle %}{{ _('Create dataset') }}{% endblock %} +{% block subtitle %}{{ _('Create Dataset') }}{% endblock %} diff --git a/ckan/templates/package/read.html b/ckan/templates/package/read.html index d70cc3e5bf2..9f178b198ec 100644 --- a/ckan/templates/package/read.html +++ b/ckan/templates/package/read.html @@ -2,8 +2,6 @@ {% set pkg = c.pkg_dict %} -{% block subtitle %}{{ pkg.title or pkg.name }}{% endblock %} - {% block primary_content_inner %} {{ super() }} {% block package_description %} diff --git a/ckan/templates/package/read_base.html b/ckan/templates/package/read_base.html index f0a58a21018..8cd14492cd9 100644 --- a/ckan/templates/package/read_base.html +++ b/ckan/templates/package/read_base.html @@ -1,6 +1,6 @@ {% extends "package/base.html" %} -{% block subtitle %}{{ pkg.title or pkg.name }}{% endblock %} +{% block subtitle %}{{ pkg.title or pkg.name }} - {{ super() }}{% endblock %} {% block links -%} {{ super() }} @@ -16,12 +16,13 @@ {% block content_action %} {% if h.check_access('package_update', {'id':pkg.id }) %} - {% link_for _('Edit'), controller='package', action='edit', id=pkg.name, class_='btn', icon='wrench' %} + {% link_for _('Manage'), controller='package', action='edit', id=pkg.name, class_='btn', icon='wrench' %} {% endif %} {% endblock %} {% block content_primary_nav %} {{ h.build_nav_icon('dataset_read', _('Dataset'), id=pkg.name) }} + {{ h.build_nav_icon('dataset_groups', _('Groups'), id=pkg.name) }} {{ h.build_nav_icon('dataset_activity', _('Activity Stream'), id=pkg.name) }} {{ h.build_nav_icon('related_list', _('Related'), id=pkg.name) }} {% endblock %} @@ -59,23 +60,6 @@ {% endif %} {% endblock %} - {% block package_groups %} - {% if pkg.groups %} -
    -

    {{ _('Groups') }}

    - -
    - {% endif %} - {% endblock %} - {% block package_social %} {% snippet "snippets/social.html" %} {% endblock %} diff --git a/ckan/templates/package/related_list.html b/ckan/templates/package/related_list.html index 0f31cb0e790..affdd974372 100644 --- a/ckan/templates/package/related_list.html +++ b/ckan/templates/package/related_list.html @@ -2,6 +2,8 @@ {% set pkg = c.pkg %} +{% block subtitle %}{{ _('Related') }} - {{ super() }}{% endblock %} + {% block primary_content_inner %}

    {% block page_heading %}{{ _('Related Media for {dataset}').format(dataset=h.dataset_display_name(c.pkg)) }}{% endblock %}

    {% block related_list %} diff --git a/ckan/templates/package/resource_read.html b/ckan/templates/package/resource_read.html index 46406746a6f..dee83c32e0d 100644 --- a/ckan/templates/package/resource_read.html +++ b/ckan/templates/package/resource_read.html @@ -27,7 +27,7 @@
      {% block resource_actions_inner %} {% if h.check_access('package_update', {'id':pkg.id }) %} -
    • {% link_for _('Edit'), controller='package', action='resource_edit', id=pkg.name, resource_id=res.id, class_='btn', icon='wrench' %}
    • +
    • {% link_for _('Manage'), controller='package', action='resource_edit', id=pkg.name, resource_id=res.id, class_='btn', icon='wrench' %}
    • {% endif %} {% if res.url %}
    • diff --git a/ckan/templates/package/search.html b/ckan/templates/package/search.html index 6274ecc0798..6c8173eb772 100644 --- a/ckan/templates/package/search.html +++ b/ckan/templates/package/search.html @@ -1,7 +1,7 @@ {% extends "page.html" %} {% import 'macros/form.html' as form %} -{% block subtitle %}{{ _("Search for a Dataset") }}{% endblock %} +{% block subtitle %}{{ _("Datasets") }}{% endblock %} {% block breadcrumb_content %}
    • {{ h.nav_link(_('Datasets'), controller='package', action='search', highlight_actions = 'new index') }}
    • diff --git a/ckan/templates/package/snippets/package_basic_fields.html b/ckan/templates/package/snippets/package_basic_fields.html index d1ac7e25577..56d12253516 100644 --- a/ckan/templates/package/snippets/package_basic_fields.html +++ b/ckan/templates/package/snippets/package_basic_fields.html @@ -64,7 +64,7 @@ {% endif %} {% if show_organizations_selector %} - {% set existing_org = data.owner_org %} + {% set existing_org = data.owner_org or data.group_id %}
      @@ -99,6 +99,4 @@
      {% endif %} - {{ form.required_message() }} - {% endblock %} diff --git a/ckan/templates/package/snippets/package_form.html b/ckan/templates/package/snippets/package_form.html index 33d726639f1..d6f3b285f56 100644 --- a/ckan/templates/package/snippets/package_form.html +++ b/ckan/templates/package/snippets/package_form.html @@ -38,10 +38,8 @@ {% block delete_button_text %}{{ _('Delete') }}{% endblock %} {% endif %} {% endblock %} - {% block cancel_button %} - {% block cancel_button_text %}{{ _('Cancel') }}{% endblock %} - {% endblock %} + {{ form.required_message() }}
      {% endblock %} diff --git a/ckan/templates/package/snippets/package_metadata_fields.html b/ckan/templates/package/snippets/package_metadata_fields.html index 4805e6637f9..b398818b297 100644 --- a/ckan/templates/package/snippets/package_metadata_fields.html +++ b/ckan/templates/package/snippets/package_metadata_fields.html @@ -29,22 +29,6 @@ {% endblock %} {% block dataset_fields %} - {% if data.groups %} -
      - -
      - {% for group in data.groups %} - - {% endfor %} -
      -
      - {% endif %} - {% set group_name = 'groups__%s__id' % data.groups|length %} - {% set group_attrs = {'data-module': 'autocomplete', 'data-module-source': '/api/2/util/group/autocomplete?q=?', 'data-module-key': 'id', 'data-module-label': 'title'} %} - {{ form.input(group_name, label=_('Add Group'), id="field-group", value=data[group_name], classes=['control-medium'], attrs=group_attrs) }} {% endblock %} {% endblock %} diff --git a/ckan/templates/package/snippets/resource_form.html b/ckan/templates/package/snippets/resource_form.html index f3bb2bddc91..5d327c3c0f4 100644 --- a/ckan/templates/package/snippets/resource_form.html +++ b/ckan/templates/package/snippets/resource_form.html @@ -4,7 +4,7 @@ {% set errors = errors or {} %} {% set action = form_action or h.url_for(controller='package', action='new_resource', id=pkg_name) %} -
      + {% block stages %} {# An empty stages variable will not show the stages #} {% if stage %} @@ -16,29 +16,12 @@ -
      - {% block basic_fields %} - - {% block basic_fields_data %} -
      - {# - This block uses a slightly odd pattern. Unlike the rest of the radio - buttons which are wrapped _inside_ the labels here we place the label - after the input. This enables us to style the label based on the state - of the radio using css. eg. input[type=radio]+label {} - #} - - - - - - -
      -
      - {% endblock %} {% block basic_fields_url %} - {{ form.input('url', id='field-url', label=_('Resource'), placeholder=_('eg. http://example.com/gold-prices-jan-2011.json'), value=data.url, error=errors.url, classes=['control-full', 'control-large'], is_required=true) }} + {% set is_upload = (data.url_type == 'upload') %} + {{ form.image_upload(data, errors, field_url='url', field_upload='upload', field_clear='clear_upload', + is_upload_enabled=h.uploads_enabled(), is_url=data.url and not is_upload, is_upload=is_upload, + upload_label=_('File Upload'), url_label=_('URL')) }} {% endblock %} {% block basic_fields_name %} @@ -61,7 +44,6 @@ {{ form.required_message() }} - {% endblock %} {% block metadata_fields %} {% if include_metadata %} diff --git a/ckan/templates/related/snippets/related_item.html b/ckan/templates/related/snippets/related_item.html index 39231a7a217..df1c1300025 100644 --- a/ckan/templates/related/snippets/related_item.html +++ b/ckan/templates/related/snippets/related_item.html @@ -11,11 +11,11 @@ #} {% set placeholder_map = { -'application':'/base/images/placeholder-application.png' +'application': h.url_for_static('/base/images/placeholder-application.png') } %} {% set tooltip = _('Go to {related_item_type}').format(related_item_type=related.type|replace('_', ' ')|title) %}