From c668b36ad05756fd1411f4d25563f1c63abae530 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Tue, 9 Apr 2013 16:50:42 +0200 Subject: [PATCH 001/189] [#744] Start writing tests for page-view tracking --- ckan/tests/functional/test_tracking.py | 581 +++++++++++++++++++++++++ 1 file changed, 581 insertions(+) create mode 100644 ckan/tests/functional/test_tracking.py diff --git a/ckan/tests/functional/test_tracking.py b/ckan/tests/functional/test_tracking.py new file mode 100644 index 00000000000..4f560771914 --- /dev/null +++ b/ckan/tests/functional/test_tracking.py @@ -0,0 +1,581 @@ +'''Functional tests for CKAN's builtin page view tracking feature.''' + +import tempfile +import csv +import datetime + +import ckan.tests as tests + + +class TestTracking(object): + + def tearDown(self): + import ckan.model as model + model.repo.rebuild_db() + + def _get_app(self): + import paste.fixture + import pylons.test + return paste.fixture.TestApp(pylons.test.pylonsapp) + + def _create_sysadmin(self, app): + '''Create a sysadmin user. + + Returns a tuple (sysadmin_user_object, api_key). + + ''' + # You can't create a user via the api + # (ckan.auth.create_user_via_api = false is in test-core.ini) and you + # can't make your first sysadmin user via either the api or the web + # interface anyway, so access the model directly to make a sysadmin + # user. + import ckan.model as model + user = model.User(name='joeadmin', email='joe@admin.net', + password='joe rules') + user.sysadmin = True + model.Session.add(user) + model.repo.commit_and_remove() + return (tests.call_action_api(app, 'user_show', id=user.id), + user.apikey) + + def _create_package(self, app, apikey, name='look_to_windward'): + '''Create a package via the action api.''' + + return tests.call_action_api(app, 'package_create', apikey=apikey, + name=name) + + def _create_resource(self, app, package, apikey): + '''Create a resource via the action api.''' + + return tests.call_action_api(app, 'resource_create', apikey=apikey, + package_id=package['id'], url='http://example.com') + + def _post_to_tracking(self, app, url, type_='page', ip='199.204.138.90', + browser='firefox'): + '''Post some data to /_tracking directly. + + This simulates what's supposed when you view a page with tracking + enabled (an ajax request posts to /_tracking). + + ''' + params = {'url': url, 'type': type_} + app.post('/_tracking', params=params, + extra_environ={ + # The tracking middleware crashes if these aren't present. + 'HTTP_USER_AGENT': browser, + 'REMOTE_ADDR': ip, + 'HTTP_ACCEPT_LANGUAGE': 'en', + 'HTTP_ACCEPT_ENCODING': 'gzip, deflate', + }) + + def _update_tracking_summary(self): + '''Update CKAN's tracking summary data. + + This simulates calling `paster tracking update` on the command line. + + ''' + # FIXME: Can this be done as more of a functional test where we + # actually test calling the command and passing the args? By calling + # the method directly, we're not testing the command-line parsing. + import ckan.lib.cli + import ckan.model + date = (datetime.datetime.now() - datetime.timedelta(days=1)).strftime( + '%Y-%m-%d') + ckan.lib.cli.Tracking('Tracking').update_all( + engine=ckan.model.meta.engine, start_date=date) + + def _rebuild_search_index(self): + '''Rebuild CKAN's search index. + + This simulates calling `paster search-index rebuild` on the command + line. + + ''' + import ckan.lib.cli + ckan.lib.cli.SearchIndexCommand('SearchIndexCommand').rebuild() + + def test_package_with_0_views(self): + app = self._get_app() + sysadmin_user, apikey = self._create_sysadmin(app) + package = self._create_package(app, apikey) + + # The API should return 0 recent views and 0 total views for the + # unviewed package. + package = tests.call_action_api(app, 'package_show', + id=package['name']) + tracking_summary = package['tracking_summary'] + assert tracking_summary['recent'] == 0, ("A package that has not " + "been viewed should have 0 recent views") + assert tracking_summary['total'] == 0, ("A package that has not " + "been viewed should have 0 total views") + + def test_resource_with_0_views(self): + app = self._get_app() + sysadmin_user, apikey = self._create_sysadmin(app) + package = self._create_package(app, apikey) + resource = self._create_resource(app, package, apikey) + + # The package_show() API should return 0 recent views and 0 total + # views for the unviewed resource. + package = tests.call_action_api(app, 'package_show', + id=package['name']) + assert len(package['resources']) == 1 + resource = package['resources'][0] + tracking_summary = resource['tracking_summary'] + assert tracking_summary['recent'] == 0, ("A resource that has not " + "been viewed should have 0 recent views") + assert tracking_summary['total'] == 0, ("A resource that has not " + "been viewed should have 0 total views") + + # The resource_show() API should return 0 recent views and 0 total + # views for the unviewed resource. + resource = tests.call_action_api(app, 'resource_show', + id=resource['id']) + tracking_summary = resource['tracking_summary'] + assert tracking_summary['recent'] == 0, ("A resource that has not " + "been viewed should have 0 recent views") + assert tracking_summary['total'] == 0, ("A resource that has not " + "been viewed should have 0 total views") + + def test_package_with_one_view(self): + import routes + + app = self._get_app() + sysadmin_user, apikey = self._create_sysadmin(app) + package = self._create_package(app, apikey) + self._create_resource(app, package, apikey) + + url = routes.url_for(controller='package', action='read', + id=package['name']) + self._post_to_tracking(app, url) + + self._update_tracking_summary() + + package = tests.call_action_api(app, 'package_show', id=package['id']) + tracking_summary = package['tracking_summary'] + assert tracking_summary['recent'] == 1, ("A package that has been " + "viewed once should have 1 recent view.") + assert tracking_summary['total'] == 1, ("A package that has been " + "viewed once should have 1 total view") + + assert len(package['resources']) == 1 + resource = package['resources'][0] + tracking_summary = resource['tracking_summary'] + assert tracking_summary['recent'] == 0, ("Viewing a package should " + "not increase the recent views of the package's resources") + assert tracking_summary['total'] == 0, ("Viewing a package should " + "not increase the total views of the package's resources") + + def test_resource_with_one_preview(self): + import routes + + app = self._get_app() + sysadmin_user, apikey = self._create_sysadmin(app) + package = self._create_package(app, apikey) + resource = self._create_resource(app, package, apikey) + + url = routes.url_for(controller='package', action='resource_read', + id=package['name'], resource_id=resource['id']) + self._post_to_tracking(app, url) + + self._update_tracking_summary() + + package = tests.call_action_api(app, 'package_show', id=package['id']) + assert len(package['resources']) == 1 + resource = package['resources'][0] + + assert package['tracking_summary']['recent'] == 0, ("Previewing a " + "resource should not increase the package's recent views") + assert package['tracking_summary']['total'] == 0, ("Previewing a " + "resource should not increase the package's total views") + # Yes, previewing a resource does _not_ increase its view count. + assert resource['tracking_summary']['recent'] == 0, ("Previewing a " + "resource should not increase the resource's recent views") + assert resource['tracking_summary']['total'] == 0, ("Previewing a " + "resource should not increase the resource's recent views") + + def test_resource_with_one_download(self): + app = self._get_app() + sysadmin_user, apikey = self._create_sysadmin(app) + package = self._create_package(app, apikey) + resource = self._create_resource(app, package, apikey) + + self._post_to_tracking(app, resource['url'], type_='resource') + self._update_tracking_summary() + + package = tests.call_action_api(app, 'package_show', id=package['id']) + assert len(package['resources']) == 1 + resource = package['resources'][0] + assert package['tracking_summary']['recent'] == 0, ("Downloading a " + "resource should not increase the package's recent views") + assert package['tracking_summary']['total'] == 0, ("Downloading a " + "resource should not increase the package's total views") + assert resource['tracking_summary']['recent'] == 1, ("Downloading a " + "resource should increase the resource's recent views") + assert resource['tracking_summary']['total'] == 1, ("Downloading a " + "resource should increase the resource's total views") + + # The resource_show() API should return the same result. + resource = tests.call_action_api(app, 'resource_show', + id=resource['id']) + tracking_summary = resource['tracking_summary'] + assert tracking_summary['recent'] == 1, ("Downloading a " + "resource should increase the resource's recent views") + assert tracking_summary['total'] == 1, ("Downloading a " + "resource should increase the resource's total views") + + def test_view_page(self): + app = self._get_app() + + # Visit the front page. + self._post_to_tracking(app, url='', type_='page') + # Visit the /organization page. + self._post_to_tracking(app, url='/organization', type_='page') + # Visit the /about page. + self._post_to_tracking(app, url='/about', type_='page') + + self._update_tracking_summary() + + # There's no way to export page-view (as opposed to resource or + # dataset) tracking summaries, eg. via the api or a paster command, the + # only way we can check them is through the model directly. + import ckan.model as model + for url in ('', '/organization', '/about'): + q = model.Session.query(model.TrackingSummary) + q = q.filter_by(url=url) + tracking_summary = q.one() + assert tracking_summary.count == 1, ("Viewing a page should " + "increase the page's view count") + # For pages (as opposed to datasets and resources) recent_views and + # running_total always stay at 1. Shrug. + assert tracking_summary.recent_views == 0, ("recent_views for a " + "page is always 0") + assert tracking_summary.running_total == 0, ("running_total for a " + "page is always 0") + + def test_package_with_many_views(self): + import routes + + app = self._get_app() + sysadmin_user, apikey = self._create_sysadmin(app) + package = self._create_package(app, apikey) + self._create_resource(app, package, apikey) + + url = routes.url_for(controller='package', action='read', + id=package['name']) + + # View the package three times from different IPs. + self._post_to_tracking(app, url, ip='111.222.333.44') + self._post_to_tracking(app, url, ip='111.222.333.55') + self._post_to_tracking(app, url, ip='111.222.333.66') + + self._update_tracking_summary() + + package = tests.call_action_api(app, 'package_show', id=package['id']) + tracking_summary = package['tracking_summary'] + assert tracking_summary['recent'] == 3, ("A package that has been " + "viewed 3 times recently should have 3 recent views") + assert tracking_summary['total'] == 3, ("A package that has been " + "viewed 3 times should have 3 total views") + + assert len(package['resources']) == 1 + resource = package['resources'][0] + tracking_summary = resource['tracking_summary'] + assert tracking_summary['recent'] == 0, ("Viewing a package should " + "not increase the recent views of the package's resources") + assert tracking_summary['total'] == 0, ("Viewing a package should " + "not increase the total views of the package's resources") + + def test_resource_with_many_downloads(self): + app = self._get_app() + sysadmin_user, apikey = self._create_sysadmin(app) + package = self._create_package(app, apikey) + resource = self._create_resource(app, package, apikey) + url = resource['url'] + + # Download the resource three times from different IPs. + self._post_to_tracking(app, url, type_='resource', ip='111.222.333.44') + self._post_to_tracking(app, url, type_='resource', ip='111.222.333.55') + self._post_to_tracking(app, url, type_='resource', ip='111.222.333.66') + + self._update_tracking_summary() + + package = tests.call_action_api(app, 'package_show', id=package['id']) + assert len(package['resources']) == 1 + resource = package['resources'][0] + tracking_summary = resource['tracking_summary'] + assert tracking_summary['recent'] == 3, ("A resource that has been " + "downloaded 3 times recently should have 3 recent downloads") + assert tracking_summary['total'] == 3, ("A resource that has been " + "downloaded 3 times should have 3 total downloads") + + tracking_summary = package['tracking_summary'] + assert tracking_summary['recent'] == 0, ("Downloading a resource " + "should not increase the resource's package's recent views") + assert tracking_summary['total'] == 0, ("Downloading a resource " + "should not increase the resource's package's total views") + + def test_page_with_many_views(self): + app = self._get_app() + + # View each page three times, from three different IPs. + for ip in ('111.111.11.111', '222.222.22.222', '333.333.33.333'): + # Visit the front page. + self._post_to_tracking(app, url='', type_='page', ip=ip) + # Visit the /organization page. + self._post_to_tracking(app, url='/organization', type_='page', + ip=ip) + # Visit the /about page. + self._post_to_tracking(app, url='/about', type_='page', ip=ip) + + self._update_tracking_summary() + + # There's no way to export page-view (as opposed to resource or + # dataset) tracking summaries, eg. via the api or a paster command, the + # only way we can check them if through the model directly. + import ckan.model as model + for url in ('', '/organization', '/about'): + q = model.Session.query(model.TrackingSummary) + q = q.filter_by(url=url) + tracking_summary = q.one() + assert tracking_summary.count == 3, ("A page that has been " + "viewed three times should have view count 3") + # For pages (as opposed to datasets and resources) recent_views and + # running_total always stay at 1. Shrug. + assert tracking_summary.recent_views == 0, ("recent_views for " + "pages is always 0") + assert tracking_summary.running_total == 0, ("running_total for " + "pages is always 0") + + def test_recent_views_expire(self): + # TODO + # Test that package, resource and page views (maybe 3 different tests) + # older than 14 days are counted as total views but not as recent + # views. + # Will probably have to access the model directly to insert tracking + # data older than 14 days. + pass + + def test_dataset_view_count_throttling(self): + '''If the same user visits the same dataset multiple times on the same + day, only one view should get counted. + + ''' + import routes + + app = self._get_app() + sysadmin_user, apikey = self._create_sysadmin(app) + package = self._create_package(app, apikey) + self._create_resource(app, package, apikey) + url = routes.url_for(controller='package', action='read', + id=package['name']) + + # Visit the dataset three times from the same IP. + self._post_to_tracking(app, url) + self._post_to_tracking(app, url) + self._post_to_tracking(app, url) + + self._update_tracking_summary() + + package = tests.call_action_api(app, 'package_show', id=package['id']) + tracking_summary = package['tracking_summary'] + assert tracking_summary['recent'] == 1, ("Repeat dataset views should " + "not add to recent views count") + assert tracking_summary['total'] == 1, ("Repeat dataset views should " + "not add to total views count") + + def test_resource_download_count_throttling(self): + '''If the same user downloads the same resource multiple times on the + same day, only one view should get counted. + + ''' + app = self._get_app() + sysadmin_user, apikey = self._create_sysadmin(app) + package = self._create_package(app, apikey) + resource = self._create_resource(app, package, apikey) + + # Download the resource three times from the same IP. + self._post_to_tracking(app, resource['url'], type_='resource') + self._post_to_tracking(app, resource['url'], type_='resource') + self._post_to_tracking(app, resource['url'], type_='resource') + + self._update_tracking_summary() + + resource = tests.call_action_api(app, 'resource_show', + id=resource['id']) + tracking_summary = resource['tracking_summary'] + assert tracking_summary['recent'] == 1, ("Repeat resource downloads " + "should not add to recent views count") + assert tracking_summary['total'] == 1, ("Repeat resource downloads " + "should not add to total views count") + + def test_sorting_datasets_by_recent_views(self): + # FIXME: Have some datasets with different numbers of recent and total + # views, to make this a better test. + import routes + import ckan.lib.search + + tests.setup_test_search_index() + + app = self._get_app() + sysadmin_user, apikey = self._create_sysadmin(app) + self._create_package(app, apikey, name='consider_phlebas') + self._create_package(app, apikey, name='the_player_of_games') + self._create_package(app, apikey, name='use_of_weapons') + + url = routes.url_for(controller='package', action='read', + id='consider_phlebas') + self._post_to_tracking(app, url) + + url = routes.url_for(controller='package', action='read', + id='the_player_of_games') + self._post_to_tracking(app, url, ip='111.11.111.111') + self._post_to_tracking(app, url, ip='222.22.222.222') + + url = routes.url_for(controller='package', action='read', + id='use_of_weapons') + self._post_to_tracking(app, url, ip='111.11.111.111') + self._post_to_tracking(app, url, ip='222.22.222.222') + self._post_to_tracking(app, url, ip='333.33.333.333') + + self._update_tracking_summary() + ckan.lib.search.rebuild() + + response = tests.call_action_api(app, 'package_search', + sort='views_recent desc') + assert response['count'] == 3 + assert response['sort'] == 'views_recent desc' + packages = response['results'] + assert packages[0]['name'] == 'use_of_weapons' + assert packages[1]['name'] == 'the_player_of_games' + assert packages[2]['name'] == 'consider_phlebas' + + def test_sorting_datasets_by_total_views(self): + # FIXME: Have some datasets with different numbers of recent and total + # views, to make this a better test. + import routes + import ckan.lib.search + + tests.setup_test_search_index() + + app = self._get_app() + sysadmin_user, apikey = self._create_sysadmin(app) + self._create_package(app, apikey, name='consider_phlebas') + self._create_package(app, apikey, name='the_player_of_games') + self._create_package(app, apikey, name='use_of_weapons') + + url = routes.url_for(controller='package', action='read', + id='consider_phlebas') + self._post_to_tracking(app, url) + + url = routes.url_for(controller='package', action='read', + id='the_player_of_games') + self._post_to_tracking(app, url, ip='111.11.111.111') + self._post_to_tracking(app, url, ip='222.22.222.222') + + url = routes.url_for(controller='package', action='read', + id='use_of_weapons') + self._post_to_tracking(app, url, ip='111.11.111.111') + self._post_to_tracking(app, url, ip='222.22.222.222') + self._post_to_tracking(app, url, ip='333.33.333.333') + + self._update_tracking_summary() + ckan.lib.search.rebuild() + + response = tests.call_action_api(app, 'package_search', + sort='views_total desc') + assert response['count'] == 3 + assert response['sort'] == 'views_total desc' + packages = response['results'] + assert packages[0]['name'] == 'use_of_weapons' + assert packages[1]['name'] == 'the_player_of_games' + assert packages[2]['name'] == 'consider_phlebas' + + def test_popular_package(self): + # Test that a package with > 10 views is marked as 'popular'. + # Currently the popular logic is in the templates, will have to move + # that into the logic and add 'popular': True/False to package dicts + # to make this testable. + # Also test that a package with < 10 views is not marked as popular. + # Test what kind of views count towards popularity, recent or total, + # and which don't. + pass + + def test_popular_resource(self): + # Test that a resource with > 10 views is marked as 'popular'. + # Currently the popular logic is in the templates, will have to move + # that into the logic and add 'popular': True/False to resource dicts + # to make this testable. + # Also test that a resource with < 10 views is not marked as popular. + # Test what kind of views count towards popularity, recent or total, + # and which don't. + pass + + def test_same_user_visiting_different_pages_on_same_day(self): + # Test that if the same user visits multiple pages on the same say, + # each visit gets counted (this should not get throttled) + # (May need to test for packages, resources and pages separately) + pass + + def test_same_user_visiting_same_page_on_different_days(self): + # Test that if the same user visits the same page on different days, + # each visit gets counted (this should not get throttled) + # (May need to test for packages, resources and pages separately) + # (Probably need to access the model directly to insert old visits + # into tracking_raw) + pass + + def test_posting_bad_data_to_tracking(self): + # Test how /_tracking handles unexpected and invalid data. + pass + + def _export_tracking_summary(self): + '''Export CKAN's tracking data and return it. + + This simulates calling `paster tracking export` on the command line. + + ''' + # FIXME: Can this be done as more of a functional test where we + # actually test calling the command and passing the args? By calling + # the method directly, we're not testing the command-line parsing. + import ckan.lib.cli + import ckan.model + f = tempfile.NamedTemporaryFile() + ckan.lib.cli.Tracking('Tracking').export_tracking( + engine=ckan.model.meta.engine, output_filename=f.name) + lines = csv.DictReader(open(f.name, 'r')) + return lines + + def test_export(self): + # Create some packages and resources, visit them and some other pages, + # export all the tracking data to CSV and check that the exported + # values are correct. + pass + + def test_tracking_urls_with_languages(self): + # Test that posting to eg /de/dataset/foo is counted the same as + # /dataset/foo. + # May need to test for dataset pages, resource previews, resource + # downloads, and other page views separately. + pass + + def test_templates_tracking_enabled(self): + # Test the the page view tracking JS is in the templates when + # ckan.tracking_enabled = true. + # Test that the sort by populatiy option is shown on the datasets page. + pass + + def test_templates_tracking_disabled(self): + # Test the the page view tracking JS is not in the templates when + # ckan.tracking_enabled = false. + # Test that the sort by populatiy option is not on the datasets page. + pass + + def test_tracking_disabled(self): + # Just to make sure, set ckan.tracking_enabled = false and then post + # a bunch of stuff to /_tracking and test that no tracking data is + # recorded. Maybe /_tracking should return something other than 200, + # as well. + # Could also test that 'tracking_summary' is _not_ in package and + # resource dicts from api when tracking is disabled. + pass From 2f38cae5833d5dd0341925640ba1ef7c1f3ea6a8 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Tue, 9 Apr 2013 17:46:24 +0200 Subject: [PATCH 002/189] [#744] Add tracking_enabled = true to test-core.ini This makes the test_tracking.py tests pass --- test-core.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test-core.ini b/test-core.ini index d4372d234c2..d8a143effff 100644 --- a/test-core.ini +++ b/test-core.ini @@ -82,6 +82,8 @@ ckan.activity_streams_email_notifications = True ckan.activity_list_limit = 15 +ckan.tracking_enabled = true + # Logging configuration [loggers] keys = root, ckan, sqlalchemy From 73308db08242a6a2ee72079b6cc87ce4b0705761 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Wed, 17 Jul 2013 15:49:13 +0200 Subject: [PATCH 003/189] [#1117] Add new_tests dir with first new logic tests Eventually (before this branch is merged into master) new_tests should become tests and the current tests should become legacy_tests. --- ckan/new_tests/__init__.py | 0 ckan/new_tests/data.py | 7 ++ ckan/new_tests/helpers.py | 49 ++++++++++++++ ckan/new_tests/logic/__init__.py | 0 ckan/new_tests/logic/action/__init__.py | 0 ckan/new_tests/logic/action/test_update.py | 79 ++++++++++++++++++++++ 6 files changed, 135 insertions(+) create mode 100644 ckan/new_tests/__init__.py create mode 100644 ckan/new_tests/data.py create mode 100644 ckan/new_tests/helpers.py create mode 100644 ckan/new_tests/logic/__init__.py create mode 100644 ckan/new_tests/logic/action/__init__.py create mode 100644 ckan/new_tests/logic/action/test_update.py diff --git a/ckan/new_tests/__init__.py b/ckan/new_tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ckan/new_tests/data.py b/ckan/new_tests/data.py new file mode 100644 index 00000000000..d5d76a8f800 --- /dev/null +++ b/ckan/new_tests/data.py @@ -0,0 +1,7 @@ +'''A collection of static data for use in tests. + +''' +TYPICAL_USER = {'name': 'fred', + 'email': 'fred@fred.com', + 'password': 'wilma', + } diff --git a/ckan/new_tests/helpers.py b/ckan/new_tests/helpers.py new file mode 100644 index 00000000000..7ad0bd00781 --- /dev/null +++ b/ckan/new_tests/helpers.py @@ -0,0 +1,49 @@ +'''A collection of test helper functions. + +''' +import ckan.model as model +import ckan.logic as logic + + +def reset_db(): + '''Reset CKAN's database. + + Each test module or class should call this function in its setup method. + + ''' + # Close any database connections that have been left open. + # This prevents CKAN from hanging waiting for some unclosed connection. + model.Session.close_all() + + # Clean out any data from the db. This prevents tests failing due to data + # leftover from other tests or from previous test runs. + model.repo.clean_db() + + # Initialize the db. This prevents CKAN from crashing if the tests are run + # and the test db has not been initialized. + model.repo.init_db() + + +def call_action(action_name, context=None, **kwargs): + '''Call the given ckan.logic.action function with the given context + and params. + + For example: + + call_action('user_create', name='seanh', email='seanh@seanh.com', + password='pass') + + This is just a nicer way for user code to call action functions, nicer than + either calling the action function directly or via get_action(). + + If accepted this function should eventually be moved to + ckan.logic.call_action() and the current get_action() function should be + deprecated. The tests may still need their own wrapper function for + logic.call_action(), e.g. to insert 'ignore_auth': True into the context. + + ''' + if context is None: + context = {} + context.setdefault('user', '127.0.0.1') + context.setdefault('ignore_auth', True) + return logic.get_action(action_name)(context=context, data_dict=kwargs) diff --git a/ckan/new_tests/logic/__init__.py b/ckan/new_tests/logic/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ckan/new_tests/logic/action/__init__.py b/ckan/new_tests/logic/action/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ckan/new_tests/logic/action/test_update.py b/ckan/new_tests/logic/action/test_update.py new file mode 100644 index 00000000000..6a7ecfd0d8f --- /dev/null +++ b/ckan/new_tests/logic/action/test_update.py @@ -0,0 +1,79 @@ +'''Unit tests for ckan/logic/action/update.py. + +''' +import ckan.new_tests.helpers as helpers +import ckan.new_tests.data as data + + +def setup_module(): + helpers.reset_db() + +# Tests for user_update: +# +# - Test for success: +# - Typical values +# - Edge cases +# - If multiple params, test with different combinations +# - Test for failure: +# - Common mistakes +# - Bizarre input +# - Unicode +# - Cover the interface +# - Can we somehow mock user_create? +# +# Correct and incorrect ID + + +def test_user_update(): + '''Test that updating a user's metadata fields works successfully. + + ''' + # Prepare + user = helpers.call_action('user_create', **data.TYPICAL_USER) + + # Execute + user['name'] = 'updated_name' + user['about'] = 'updated_about' + helpers.call_action('user_update', **user) + + # Assert + updated_user = helpers.call_action('user_show', id=user['id']) + # Note that we check each updated field seperately, we don't compare the + # entire dicts, only assert what we're actually testing. + assert user['name'] == updated_user['name'] + assert user['about'] == updated_user['about'] + + +def test_user_update_with_invalid_id(): + pass + + +def test_user_update_with_nonexistent_id(): + pass + + +def test_user_update_with_no_id(): + pass + + +def test_user_update_with_custom_schema(): + pass + + +def test_user_update_with_invalid_name(): + pass + + +def test_user_update_with_invalid_password(): + pass + + +def test_user_update_with_deferred_commit(): + pass + + +# TODO: Valid and invalid values for other fields. + + +def test_user_update_activity_stream(): + pass From 3491cd695ec0c9758586d219c027165256fd7012 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Wed, 17 Jul 2013 19:14:05 +0200 Subject: [PATCH 004/189] [#1117] Add new tests for updating user names and passwords ...and also for what happens when you call user_update without a valid id. --- ckan/new_tests/logic/action/test_update.py | 211 ++++++++++++++------- 1 file changed, 139 insertions(+), 72 deletions(-) diff --git a/ckan/new_tests/logic/action/test_update.py b/ckan/new_tests/logic/action/test_update.py index 6a7ecfd0d8f..d17a4092541 100644 --- a/ckan/new_tests/logic/action/test_update.py +++ b/ckan/new_tests/logic/action/test_update.py @@ -1,79 +1,146 @@ '''Unit tests for ckan/logic/action/update.py. ''' +import nose.tools + +import ckan.logic as logic import ckan.new_tests.helpers as helpers import ckan.new_tests.data as data -def setup_module(): - helpers.reset_db() - -# Tests for user_update: -# -# - Test for success: -# - Typical values -# - Edge cases -# - If multiple params, test with different combinations -# - Test for failure: -# - Common mistakes -# - Bizarre input -# - Unicode -# - Cover the interface -# - Can we somehow mock user_create? -# -# Correct and incorrect ID - - -def test_user_update(): - '''Test that updating a user's metadata fields works successfully. - - ''' - # Prepare - user = helpers.call_action('user_create', **data.TYPICAL_USER) - - # Execute - user['name'] = 'updated_name' - user['about'] = 'updated_about' - helpers.call_action('user_update', **user) - - # Assert - updated_user = helpers.call_action('user_show', id=user['id']) - # Note that we check each updated field seperately, we don't compare the - # entire dicts, only assert what we're actually testing. - assert user['name'] == updated_user['name'] - assert user['about'] == updated_user['about'] - - -def test_user_update_with_invalid_id(): - pass - - -def test_user_update_with_nonexistent_id(): - pass - - -def test_user_update_with_no_id(): - pass - - -def test_user_update_with_custom_schema(): - pass - - -def test_user_update_with_invalid_name(): - pass - - -def test_user_update_with_invalid_password(): - pass - - -def test_user_update_with_deferred_commit(): - pass - - -# TODO: Valid and invalid values for other fields. - - -def test_user_update_activity_stream(): - pass +class TestClass(object): + + @classmethod + def setup_class(cls): + # Initialize the test db (if it isn't already) and clean out any data + # left in it. + helpers.reset_db() + + def setup(self): + import ckan.model as model + # Reset the db before each test method. + model.repo.rebuild_db() + + def test_update_user_name(self): + '''Test that updating a user's name works successfully.''' + + # The canonical form of a test has four steps: + # 1. Setup anything preconditions needed for the test. + # 2. Call the function that's being tested, once only. + # 3. Make assertions about the return value and/or side-effects of + # of the function that's being tested. + # 4. Do absolutely nothing else! + + # 1. Setup. + user = helpers.call_action('user_create', **data.TYPICAL_USER) + + # 2. Call the function that is being tested, once only. + # Note we have to pass the email address and password (in plain text!) + # to user_update even though we're not updating those fields, otherwise + # validation fails. + helpers.call_action('user_update', id=user['name'], + email=user['email'], + password=data.TYPICAL_USER['password'], + name='updated', + ) + + # 3. Make assertions about the return value and/or side-effects. + updated_user = helpers.call_action('user_show', id=user['id']) + # Note that we check just the field we were trying to update, not the + # entire dict, only assert what we're actually testing. + assert updated_user['name'] == 'updated' + + # 4. Do absolutely nothing else! + + def test_user_update_with_id_that_does_not_exist(self): + # FIXME: Does this need to be more realistic? + # - Actually create a user before trying to update it with the wrong + # id? + # - Actually pass other params (name, about..) to user_update? + with nose.tools.assert_raises(logic.NotFound) as context: + helpers.call_action('user_update', + id="there's no user with this id") + # TODO: Could assert the actual error message, not just the exception? + # (Could also do this with many of the tests below.) + + def test_user_update_with_no_id(self): + with nose.tools.assert_raises(logic.ValidationError) as context: + helpers.call_action('user_update') + + def test_user_update_with_invalid_name(self): + user = helpers.call_action('user_create', **data.TYPICAL_USER) + + # FIXME: This actually breaks the canonical test form rule by calling + # the function-under-test multiple times. + # Breaking this up into multiple tests would actually be clearer in + # terms of each test testing one thing and the names of the test + # methods documenting what the test does and how the function is + # supposed to behave. + # BUT the test code would be very repetitive. + invalid_names = ('', 'a', False, 0, -1, 23, 'new', 'edit', 'search', + 'a'*200, 'Hi!', ) + for name in invalid_names: + user['name'] = name + with nose.tools.assert_raises(logic.ValidationError) as context: + helpers.call_action('user_update', **user) + + def test_user_update_to_name_that_already_exists(self): + fred = helpers.call_action('user_create', **data.TYPICAL_USER) + bob = helpers.call_action('user_create', name='bob', + email='bob@bob.com', password='pass') + + # Try to update fred and change his user name to bob, which is already + # bob's user name + fred['name'] = bob['name'] + with nose.tools.assert_raises(logic.ValidationError) as context: + helpers.call_action('user_update', **fred) + + def test_update_user_password(self): + '''Test that updating a user's password works successfully.''' + + user = helpers.call_action('user_create', **data.TYPICAL_USER) + + # Note we have to pass the email address to user_update even though + # we're not updating it, otherwise validation fails. + helpers.call_action('user_update', id=user['name'], + email=user['email'], + password='new password', + ) + + # user_show() never returns the user's password, so we have to access + # the model directly to test it. + import ckan.model as model + updated_user = model.User.get(user['id']) + assert updated_user.validate_password('new password') + + def test_user_update_with_short_password(self): + user = helpers.call_action('user_create', **data.TYPICAL_USER) + + user['password'] = 'xxx' # This password is too short. + with nose.tools.assert_raises(logic.ValidationError) as context: + helpers.call_action('user_update', **user) + + def test_user_update_with_empty_password(self): + user = helpers.call_action('user_create', **data.TYPICAL_USER) + + user['password'] = '' + with nose.tools.assert_raises(logic.ValidationError) as context: + helpers.call_action('user_update', **user) + + def test_user_update_with_null_password(self): + user = helpers.call_action('user_create', **data.TYPICAL_USER) + + user['password'] = None + with nose.tools.assert_raises(logic.ValidationError) as context: + helpers.call_action('user_update', **user) + + # TODO: Valid and invalid values for the rest of the user model's fields. + + def test_user_update_activity_stream(self): + pass + + def test_user_update_with_custom_schema(self): + pass + + def test_user_update_with_deferred_commit(self): + pass From bc3baa951de74a61a96c10c2726b991d7ee88894 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Wed, 17 Jul 2013 19:17:01 +0200 Subject: [PATCH 005/189] [#1117] user_update don't crash on non-string name Don't crash if the user tries to update their user name to a value that is not a string (which they can do, using the API) --- ckan/logic/validators.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ckan/logic/validators.py b/ckan/logic/validators.py index f9de157795c..78d37eb9f08 100644 --- a/ckan/logic/validators.py +++ b/ckan/logic/validators.py @@ -276,6 +276,9 @@ def extras_unicode_convert(extras, context): name_match = re.compile('[a-z0-9_\-]*$') def name_validator(val, context): + if not isinstance(val, basestring): + raise Invalid(_('Names must be strings')) + # check basic textual rules if val in ['new', 'edit', 'search']: raise Invalid(_('That name cannot be used')) @@ -478,6 +481,9 @@ def user_name_validator(key, data, errors, context): session = context["session"] user = context.get("user_obj") + if not isinstance(data[key], basestring): + raise Invalid(_('User names must be strings')) + query = session.query(model.User.name).filter_by(name=data[key]) if user: user_id = user.id From f3165ce46cf2c2f16a79013fb23f79bdd821ca90 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Wed, 17 Jul 2013 19:17:38 +0200 Subject: [PATCH 006/189] [#1117] user_update: don't crash on non-string password Fix a crash that happens if the user tries to update their password to a value that isn't a string (which they can do, using the API) --- ckan/logic/validators.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ckan/logic/validators.py b/ckan/logic/validators.py index 78d37eb9f08..5874f89d81c 100644 --- a/ckan/logic/validators.py +++ b/ckan/logic/validators.py @@ -507,7 +507,11 @@ def user_both_passwords_entered(key, data, errors, context): def user_password_validator(key, data, errors, context): value = data[key] - if not value == '' and not isinstance(value, Missing) and not len(value) >= 4: + if isinstance(value, Missing): + errors[('password',)].append(_('Your password must be 4 characters or longer')) + elif not isinstance(value, basestring): + errors[('password',)].append(_('Passwords must be strings')) + elif len(value) < 4: errors[('password',)].append(_('Your password must be 4 characters or longer')) def user_passwords_match(key, data, errors, context): From 41a1a727cbecfa8331b45b16bd2570c11b98af07 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 18 Jul 2013 16:01:59 +0200 Subject: [PATCH 007/189] [#1117] Typo --- ckan/new_tests/logic/action/test_update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/new_tests/logic/action/test_update.py b/ckan/new_tests/logic/action/test_update.py index d17a4092541..8d48190248a 100644 --- a/ckan/new_tests/logic/action/test_update.py +++ b/ckan/new_tests/logic/action/test_update.py @@ -25,7 +25,7 @@ def test_update_user_name(self): '''Test that updating a user's name works successfully.''' # The canonical form of a test has four steps: - # 1. Setup anything preconditions needed for the test. + # 1. Setup any preconditions needed for the test. # 2. Call the function that's being tested, once only. # 3. Make assertions about the return value and/or side-effects of # of the function that's being tested. From 6d9f3b0ff9c2b2509cbdfb088fb76a5d161eba7e Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 18 Jul 2013 17:01:56 +0200 Subject: [PATCH 008/189] [#1117] Make various improvements to test_update.py - Add some notes to the top of test_update.py about what I'm trying to do with it, these will end up in the testing guidelines - Make the static test data read-only - Rename some of the test methods so that they're all test_user_update_* - Add test_user_update_activity() test - Other small tweaks --- ckan/new_tests/data.py | 15 ++- ckan/new_tests/logic/action/test_update.py | 122 ++++++++++++++++----- 2 files changed, 103 insertions(+), 34 deletions(-) diff --git a/ckan/new_tests/data.py b/ckan/new_tests/data.py index d5d76a8f800..e7e9f120e1c 100644 --- a/ckan/new_tests/data.py +++ b/ckan/new_tests/data.py @@ -1,7 +1,14 @@ '''A collection of static data for use in tests. +These are functions that return objects because the data is intended to be +read-only: if they were simply module-level objects then one test may modify +eg. a dict or list and then break the tests that follow it. + ''' -TYPICAL_USER = {'name': 'fred', - 'email': 'fred@fred.com', - 'password': 'wilma', - } + + +def typical_user(): + return {'name': 'fred', + 'email': 'fred@fred.com', + 'password': 'wilma', + } diff --git a/ckan/new_tests/logic/action/test_update.py b/ckan/new_tests/logic/action/test_update.py index 8d48190248a..335100a2c00 100644 --- a/ckan/new_tests/logic/action/test_update.py +++ b/ckan/new_tests/logic/action/test_update.py @@ -1,6 +1,44 @@ '''Unit tests for ckan/logic/action/update.py. +Notes: + +- All user_update tests are named test_user_update_* + +- Using the canonical test form from the Pylons guidelines + (i.e. each method tests One Thing and the method name explains what it tests) + +- Not testing auth here, that can be done in ckan.tests.logic.auth.*.py + +- But I am testing validation here, because some action functions do some of + their own validation, not all the validation is done in the schemas + +- The tests for each action function try to cover: + - Testing for success: + - Typical values + - Edge cases + - Multiple parameters in different combinations + - Testing for failure: + - Common mistakes + - Bizarre input + - Unicode + +- Cover the interface of the function (i.e. test all params and features) + +- Not storing anything (e.g. test data) against self in the test class, + instead have each test method call helper functions to get any test data + it needs + + - I think it would be okay to create *read-only* data against self in + setup_class though + +- The tests are not ordered, and each one can be run on its own (db is rebuilt + after each test method) + +- Within reason, keep tests as clear and simple as possible even if it means + they get repetitive + ''' +import datetime import nose.tools import ckan.logic as logic @@ -8,6 +46,15 @@ import ckan.new_tests.data as data +def datetime_from_string(s): + '''Return a standard datetime.datetime object initialised from a string in + the same format used for timestamps in dictized activities (the format + produced by datetime.datetime.isoformat()) + + ''' + return datetime.datetime.strptime(s, '%Y-%m-%dT%H:%M:%S.%f') + + class TestClass(object): @classmethod @@ -21,7 +68,7 @@ def setup(self): # Reset the db before each test method. model.repo.rebuild_db() - def test_update_user_name(self): + def test_user_update_name(self): '''Test that updating a user's name works successfully.''' # The canonical form of a test has four steps: @@ -32,15 +79,15 @@ def test_update_user_name(self): # 4. Do absolutely nothing else! # 1. Setup. - user = helpers.call_action('user_create', **data.TYPICAL_USER) + user = helpers.call_action('user_create', **data.typical_user()) # 2. Call the function that is being tested, once only. - # Note we have to pass the email address and password (in plain text!) - # to user_update even though we're not updating those fields, otherwise - # validation fails. + # FIXME we have to pass the email address and password to user_update + # even though we're not updating those fields, otherwise validation + # fails. helpers.call_action('user_update', id=user['name'], email=user['email'], - password=data.TYPICAL_USER['password'], + password=data.typical_user()['password'], name='updated', ) @@ -53,30 +100,22 @@ def test_update_user_name(self): # 4. Do absolutely nothing else! def test_user_update_with_id_that_does_not_exist(self): - # FIXME: Does this need to be more realistic? - # - Actually create a user before trying to update it with the wrong - # id? - # - Actually pass other params (name, about..) to user_update? + user_dict = data.typical_user() + user_dict['id'] = "there's no user with this id" with nose.tools.assert_raises(logic.NotFound) as context: - helpers.call_action('user_update', - id="there's no user with this id") + helpers.call_action('user_update', **user_dict) # TODO: Could assert the actual error message, not just the exception? # (Could also do this with many of the tests below.) def test_user_update_with_no_id(self): + user_dict = data.typical_user() + assert 'id' not in user_dict with nose.tools.assert_raises(logic.ValidationError) as context: - helpers.call_action('user_update') + helpers.call_action('user_update', **user_dict) def test_user_update_with_invalid_name(self): - user = helpers.call_action('user_create', **data.TYPICAL_USER) - - # FIXME: This actually breaks the canonical test form rule by calling - # the function-under-test multiple times. - # Breaking this up into multiple tests would actually be clearer in - # terms of each test testing one thing and the names of the test - # methods documenting what the test does and how the function is - # supposed to behave. - # BUT the test code would be very repetitive. + user = helpers.call_action('user_create', **data.typical_user()) + invalid_names = ('', 'a', False, 0, -1, 23, 'new', 'edit', 'search', 'a'*200, 'Hi!', ) for name in invalid_names: @@ -85,7 +124,7 @@ def test_user_update_with_invalid_name(self): helpers.call_action('user_update', **user) def test_user_update_to_name_that_already_exists(self): - fred = helpers.call_action('user_create', **data.TYPICAL_USER) + fred = helpers.call_action('user_create', **data.typical_user()) bob = helpers.call_action('user_create', name='bob', email='bob@bob.com', password='pass') @@ -95,12 +134,12 @@ def test_user_update_to_name_that_already_exists(self): with nose.tools.assert_raises(logic.ValidationError) as context: helpers.call_action('user_update', **fred) - def test_update_user_password(self): + def test_user_update_password(self): '''Test that updating a user's password works successfully.''' - user = helpers.call_action('user_create', **data.TYPICAL_USER) + user = helpers.call_action('user_create', **data.typical_user()) - # Note we have to pass the email address to user_update even though + # FIXME we have to pass the email address to user_update even though # we're not updating it, otherwise validation fails. helpers.call_action('user_update', id=user['name'], email=user['email'], @@ -114,21 +153,21 @@ def test_update_user_password(self): assert updated_user.validate_password('new password') def test_user_update_with_short_password(self): - user = helpers.call_action('user_create', **data.TYPICAL_USER) + user = helpers.call_action('user_create', **data.typical_user()) user['password'] = 'xxx' # This password is too short. with nose.tools.assert_raises(logic.ValidationError) as context: helpers.call_action('user_update', **user) def test_user_update_with_empty_password(self): - user = helpers.call_action('user_create', **data.TYPICAL_USER) + user = helpers.call_action('user_create', **data.typical_user()) user['password'] = '' with nose.tools.assert_raises(logic.ValidationError) as context: helpers.call_action('user_update', **user) def test_user_update_with_null_password(self): - user = helpers.call_action('user_create', **data.TYPICAL_USER) + user = helpers.call_action('user_create', **data.typical_user()) user['password'] = None with nose.tools.assert_raises(logic.ValidationError) as context: @@ -136,8 +175,31 @@ def test_user_update_with_null_password(self): # TODO: Valid and invalid values for the rest of the user model's fields. + def test_user_update_activity_stream(self): - pass + '''Test that the right activity is emitted when updating a user.''' + + user = helpers.call_action('user_create', **data.typical_user()) + before = datetime.datetime.now() + + # FIXME we have to pass the email address and password to user_update + # even though we're not updating those fields, otherwise validation + # fails. + helpers.call_action('user_update', id=user['name'], + email=user['email'], + password=data.typical_user()['password'], + name='updated', + ) + + activity_stream = helpers.call_action('user_activity_list', + id=user['id']) + latest_activity = activity_stream[0] + assert latest_activity['activity_type'] == 'changed user' + assert latest_activity['object_id'] == user['id'] + assert latest_activity['user_id'] == user['id'] + after = datetime.datetime.now() + timestamp = datetime_from_string(latest_activity['timestamp']) + assert timestamp >= before and timestamp <= after def test_user_update_with_custom_schema(self): pass From 683852c555928f1cff99e5863e63d8568f93a9e9 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 18 Jul 2013 17:05:04 +0200 Subject: [PATCH 009/189] [#1117] Add some more user_update invalid password tests --- ckan/new_tests/logic/action/test_update.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ckan/new_tests/logic/action/test_update.py b/ckan/new_tests/logic/action/test_update.py index 335100a2c00..90c7d3c0800 100644 --- a/ckan/new_tests/logic/action/test_update.py +++ b/ckan/new_tests/logic/action/test_update.py @@ -173,8 +173,15 @@ def test_user_update_with_null_password(self): with nose.tools.assert_raises(logic.ValidationError) as context: helpers.call_action('user_update', **user) - # TODO: Valid and invalid values for the rest of the user model's fields. + def test_user_update_with_invalid_password(self): + user = helpers.call_action('user_create', **data.typical_user()) + + for password in (False, -1, 23, 30.7): + user['password'] = password + with nose.tools.assert_raises(logic.ValidationError) as context: + helpers.call_action('user_update', **user) + # TODO: Valid and invalid values for the rest of the user model's fields. def test_user_update_activity_stream(self): '''Test that the right activity is emitted when updating a user.''' From df72fcbd336e29f3efa3b2d221dcd439c183762e Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 18 Jul 2013 17:06:02 +0200 Subject: [PATCH 010/189] [#1117] Fix some bad indentation --- ckan/new_tests/logic/action/test_update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/new_tests/logic/action/test_update.py b/ckan/new_tests/logic/action/test_update.py index 90c7d3c0800..113717a7545 100644 --- a/ckan/new_tests/logic/action/test_update.py +++ b/ckan/new_tests/logic/action/test_update.py @@ -126,7 +126,7 @@ def test_user_update_with_invalid_name(self): def test_user_update_to_name_that_already_exists(self): fred = helpers.call_action('user_create', **data.typical_user()) bob = helpers.call_action('user_create', name='bob', - email='bob@bob.com', password='pass') + email='bob@bob.com', password='pass') # Try to update fred and change his user name to bob, which is already # bob's user name From 7d1bdc511058e19f61080593f26a04b06f86bb43 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 18 Jul 2013 18:11:56 +0200 Subject: [PATCH 011/189] [#1117] Add user_update custom schema test --- ckan/new_tests/logic/action/test_update.py | 38 +++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/ckan/new_tests/logic/action/test_update.py b/ckan/new_tests/logic/action/test_update.py index 113717a7545..2725aa7c473 100644 --- a/ckan/new_tests/logic/action/test_update.py +++ b/ckan/new_tests/logic/action/test_update.py @@ -209,7 +209,43 @@ def test_user_update_activity_stream(self): assert timestamp >= before and timestamp <= after def test_user_update_with_custom_schema(self): - pass + '''Test that custom schemas passed to user_update do get used. + + user_update allows a custom validation schema to be passed to it in the + context dict. This is just a simple test that if you pass a custom + schema user_update does at least call a custom method that's given in + the custom schema. We assume this means it did use the custom schema + instead of the default one for validation, so user_update's custom + schema feature does work. + + ''' + import mock + import ckan.logic.schema + + user = helpers.call_action('user_create', **data.typical_user()) + + # A mock validator method, it doesn't do anything but it records what + # params it gets called with and how many times. + mock_validator = mock.MagicMock() + + # Build a custom schema by taking the default schema and adding our + # mock method to its 'id' field. + schema = ckan.logic.schema.default_update_user_schema() + schema['id'].append(mock_validator) + + # Call user_update and pass our custom schema in the context. + # FIXME: We have to pass email and password even though we're not + # trying to update them, or validation fails. + helpers.call_action('user_update', context={'schema': schema}, + id=user['name'], email=user['email'], + password=data.typical_user()['password'], + name='updated', + ) + + # Since we passed user['name'] to user_update as the 'id' param, + # our mock validator method should have been called once with + # user['name'] as arg. + mock_validator.assert_called_once_with(user['name']) def test_user_update_with_deferred_commit(self): pass From 6276cb2e143bef2e108dc4c612c32832926f9b79 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Fri, 19 Jul 2013 14:51:44 +0200 Subject: [PATCH 012/189] [#1117] Add test for user_update's defer_commit option --- ckan/new_tests/logic/action/test_update.py | 77 +++++++++++++++++++++- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/ckan/new_tests/logic/action/test_update.py b/ckan/new_tests/logic/action/test_update.py index 2725aa7c473..489fa82e15b 100644 --- a/ckan/new_tests/logic/action/test_update.py +++ b/ckan/new_tests/logic/action/test_update.py @@ -39,7 +39,9 @@ ''' import datetime + import nose.tools +import mock import ckan.logic as logic import ckan.new_tests.helpers as helpers @@ -68,6 +70,13 @@ def setup(self): # Reset the db before each test method. model.repo.rebuild_db() + def teardown(self): + # Since some of the test methods below use the mock module to patch + # things, we use this teardown() method to remove remove all patches. + # (This makes sure the patches always get removed even if the test + # method aborts with an exception or something.) + mock.patch.stopall() + def test_user_update_name(self): '''Test that updating a user's name works successfully.''' @@ -219,7 +228,6 @@ def test_user_update_with_custom_schema(self): schema feature does work. ''' - import mock import ckan.logic.schema user = helpers.call_action('user_create', **data.typical_user()) @@ -248,4 +256,69 @@ def test_user_update_with_custom_schema(self): mock_validator.assert_called_once_with(user['name']) def test_user_update_with_deferred_commit(self): - pass + '''Test that user_update()'s deferred_commit option works. + + In this test we mock out the rest of CKAN and test the user_update() + action function in isolation. What we're testing is simply that when + called with 'deferred_commit': True in its context, user_update() does + not call ckan.model.repo.commit(). + + ''' + # Patch ckan.model, so user_update will be accessing a mock object + # instead of the real model. + # It's ckan.logic.__init__.py:get_action() that actually adds the model + # into the context dict, and that module does + # `import ckan.model as model`, so what we actually need to patch is + # 'ckan.logic.model' (the name that the model has when get_action() + # accesses it), not 'ckan.model'. + model_patch = mock.patch('ckan.logic.model') + mock_model = model_patch.start() + + # Patch the validate() function, so validate() won't really be called + # when user_update() calls it. update.py does + # `_validate = ckan.lib.navl.dictization_functions.validate` so we + # actually to patch this new name that's assigned to the function, not + # its original name. + validate_patch = mock.patch('ckan.logic.action.update._validate') + mock_validate = validate_patch.start() + + # user_update() is going to call validate() with some params and it's + # going to use the result that validate() returns. So we need to give + # a mock validate function that will accept the right number of params + # and return the right result. + def mock_validate_function(data_dict, schema, context): + '''Simply return the data_dict as given (without doing any + validation) and an empty error dict.''' + return data_dict, {} + mock_validate.side_effect = mock_validate_function + + # Patch model_save, again update.py does + # `import ckan.logic.action.update.model_save as model_save` so we + # need to patch it by its new name + # 'ckan.logic.action.update.model_save'. + model_save_patch = mock.patch('ckan.logic.action.update.model_save') + model_save_patch.start() + + # Patch model_dictize, again using the new name that update.py imports + # it as. + model_dictize_patch = mock.patch( + 'ckan.logic.action.update.model_dictize') + model_dictize_patch.start() + + # Patch the get_action() function (using the name that update.py + # assigns to it) with a default mock function that does nothing and + # returns None. + get_action_patch = mock.patch('ckan.logic.action.update._get_action') + get_action_patch.start() + + # After all that patching, we can finally call user_update passing + # 'defer_commit': True. The logic code in user_update will be run but + # it'll have no effect because everything it calls is mocked out. + helpers.call_action('user_update', + context={'defer_commit': True}, + id='foobar', name='new_name', + ) + + # Assert that user_update did *not* call our mock model object's + # model.repo.commit() method. + assert not mock_model.repo.commit.called From 482c4969afb32d796c231839d2d794d28daf90ae Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Fri, 19 Jul 2013 17:58:27 +0200 Subject: [PATCH 013/189] [#1117] Add draft testing coding standards doc --- ckan/new_tests/logic/action/test_update.py | 41 +-- doc/testing-coding-standards.rst | 340 +++++++++++++++++++++ 2 files changed, 341 insertions(+), 40 deletions(-) create mode 100644 doc/testing-coding-standards.rst diff --git a/ckan/new_tests/logic/action/test_update.py b/ckan/new_tests/logic/action/test_update.py index 489fa82e15b..9c8e95fcb78 100644 --- a/ckan/new_tests/logic/action/test_update.py +++ b/ckan/new_tests/logic/action/test_update.py @@ -1,43 +1,4 @@ -'''Unit tests for ckan/logic/action/update.py. - -Notes: - -- All user_update tests are named test_user_update_* - -- Using the canonical test form from the Pylons guidelines - (i.e. each method tests One Thing and the method name explains what it tests) - -- Not testing auth here, that can be done in ckan.tests.logic.auth.*.py - -- But I am testing validation here, because some action functions do some of - their own validation, not all the validation is done in the schemas - -- The tests for each action function try to cover: - - Testing for success: - - Typical values - - Edge cases - - Multiple parameters in different combinations - - Testing for failure: - - Common mistakes - - Bizarre input - - Unicode - -- Cover the interface of the function (i.e. test all params and features) - -- Not storing anything (e.g. test data) against self in the test class, - instead have each test method call helper functions to get any test data - it needs - - - I think it would be okay to create *read-only* data against self in - setup_class though - -- The tests are not ordered, and each one can be run on its own (db is rebuilt - after each test method) - -- Within reason, keep tests as clear and simple as possible even if it means - they get repetitive - -''' +'''Unit tests for ckan/logic/action/update.py.''' import datetime import nose.tools diff --git a/doc/testing-coding-standards.rst b/doc/testing-coding-standards.rst new file mode 100644 index 00000000000..7356a9edfc3 --- /dev/null +++ b/doc/testing-coding-standards.rst @@ -0,0 +1,340 @@ +======================== +Testing coding standards +======================== + + +------------------------------------------------ +Transitioning from the legacy tests to new tests +------------------------------------------------ + +CKAN is an old code base with a large legacy test suite in +:mod:`ckan.legacy_tests`. The legacy tests are difficult to maintain and +extend, but are too many to be replaced all at once in a single effort. So +we're following this strategy: + +.. todo:: The legacy tests haven't actually been moved to ``ckan.legacy_tests`` + yet. + +#. The legacy test suite has been moved to :mod:`ckan.legacy_tests`. +#. The new test suite has been started in :mod:`ckan.tests`. +#. For now, we'll run both the legacy tests and the new tests before + merging something into the master branch. +#. Whenever we add new code, or change existing code, we'll add new-style tests + for it. +#. If you change the behavior of some code and break some legacy tests, + consider adding new tests for that code and deleting the legacy tests, + rather than updating the legacy tests. +#. Now and then, we'll write a set of new tests to cover part of the code, + and delete the relevant legacy tests. For example if you want to refactor + some code that doesn't have good tests, write a set of new-style tests for + it first, refactor, then delete the relevant legacy tests. + +In this way we can incrementally extend the new tests to cover CKAN one "island +of code" at a time, and eventually we can delete the :mod:`legacy_tests` +directory entirely. + + +-------------------------------------- +Guidelines for writing new-style tests +-------------------------------------- + +.. important:: + + All new code, or changes to existing code, should have new or updated tests + before getting merged into master. + +.. todo:: + + Maybe give a short list of what we want from the new-style tests at the top + here. + +This section gives guidelines and examples to follow when writing or reviewing +new-style tests. The subsections below cover: + +#. `What should be tested?`_ Which parts of CKAN code should have tests, + and which don't have to? +#. `How should tests be organized?`_ When adding tests for some code, or + looking for the tests for some code, how do you know where the tests should + be? +#. `How detailed should tests be?`_ When writing the tests for a function or + method, how many tests should you write and what should the tests cover? +#. `How should tests be written?`_ What's the formula and guidelines for + writing a *good* test? + + +What should be tested? +====================== + +.. note:: + + When we say that *all* functions/methods of a module/class should have + tests, we mean all *public* functions/methods that the module/class exports + for use by other modules/classes in CKAN or by extensions or templates. + + *Private* helper methods (with names beginning with ``_``) never have to + have their own tests, although they can have tests if helpful. + +:mod:`ckan.logic.action` + All action functions should have tests. Note that the tests for an action + function *don't* need to cover authorization, because the authorization + functions have their own tests. But action function tests *do* need to cover + validation, more on that later. + +:mod:`ckan.logic.auth` + All auth functions should have tests. + +:mod:`ckan.logic.converters`, :mod:`ckan.logic.validators` + All converter and validator functions should have unit tests. + Although these functions are tested indirectly by the action function + tests, this may not catch all the converters and validators and all their + options, and converters and validators are not only used by the action + functions but are also available to plugins. Having unit tests will also + help to clarify the intended behavior of each converter and validator. + +:mod:`ckan.logic.schema.py` + We *don't* write tests for each schema. The validation done by the schemas + is instead tested indirectly by the action function tests. The reason for + this is that CKAN actually does validation in multiple places: some + validation is done using schemas, some validation is done in the action + functions themselves, some is done in dictization, and some in the model. + By testing all the different valid and invalid inputs at the action function + level, we catch it all in one place. + +:mod:`ckan.controllers` + All controller methods should have tests. + +:mod:`ckan.model` and :mod:`ckan.lib` + All "non-trivial" model and lib functions and methods should have tests. + + .. todo:: Define "trivial" with an example. + + Some code is used by extensions or templates, for example the template + helpers in :mod:`ckan.lib.helpers`. If a function or method is available to + extensions or templates then it should have tests, even if you think + it's trivial. + +:mod:`ckan.plugins` + The plugin interfaces in :mod:`ckan.plugins.interfaces` are not directly + testable because they don't contain any code, *but*: + + * Each plugin interface should have an example plugin in :mod:`ckan.ckanext` + and the example plugin should have its own functional tests. + + * The tests for the code that calls the plugin interface methods should test + that the methods are called correctly. + + For example :func:`ckan.logic.action.get.package_show` calls + :meth:`ckan.plugins.interfaces.IDatasetForm.read`, so the + :func:`~ckan.logic.action.get.package_show` tests should include tests + that :meth:`~ckan.plugins.interfaces.IDatasetForm.read` is called at the + right times and with the right parameters. + + Everything in :mod:`ckan.plugins.toolkit` should have tests, because these + functions are part of the API for extensions to use. But + :mod:`~ckan.plugins.toolkit` imports most of these functions from elsewhere + in CKAN, so the tests should be elsewhere also, in the test modules for the + modules where the functions are defined. + +:mod:`ckan.migration` + All migration scripts should have tests. + +:mod:`ckan.ckanext` + Within extensions, follow the same guidelines as for CKAN core. For example + if an extension adds an action function then the action function should have + tests, etc. + + +How should tests be organized? +============================== + +The organization of test modules in :mod:`ckan.tests` mirrors the organization +of the source modules in :mod:`ckan`:: + + ckan/ + tests/ + controllers/ + test_package.py <-- Tests for ckan/controllers/package.py + ... + lib/ + test_helpers.py <-- Tests for ckan/lib/helpers.py + ... + logic/ + action/ + test_get.py + ... + auth/ + test_get.py + ... + test_converters.py + test_validators.py + migration/ + versions/ + test_001_add_existing_tables.py + ... + model/ + test_package.py + ... + ... + +There are a few exceptional test modules that don't fit into this structure, +for example PEP8 tests and coding standards tests. These modules can just go in +the top-level ``ckan/tests/`` directory. There shouldn't be too many of these. + + +How detailed should tests be? +============================= + +When you're writing the tests for a function or method, how many tests should +you write and what should the tests cover? Generally, what we're trying to do +is test the *interfaces* between modules in a way that supports modularization: +if you change the code within a function, method, class or module, if you don't +break any of that code's unit tests you should be able to expect that CKAN as a +whole will not be broken. + +As a general guideline, the tests for a function or method should: + +- Test for success: + + - Test the function with typical, valid input values + - Test with valid, edge-case inputs + - If the function has multiple parameters, test them in different + combinations + +- Test for failure: + + - Test that the function fails correctly (e.g. raises the expected type of + exception) when given likely invalid inputs (for example, if the user + passes an invalid user_id as a parameter) + - Test that the function fails correctly when given bizarre input + +- Test that the function behaves correctly when given unicode characters as + input + +- Cover the interface of the function: test all the parameters and features of + the function + +.. todo:: + + What about sanity tests? For example I should be able to convert a value + to an extra using convert_to_extras() and then convert it back again using + convert_from_extras() and get the same value back. + + Private functions and methods that are only used within the module (and + whose names should begin with underscores) *may* have tests, but don't have + to. + + +How should tests be written? +============================ + +In general, follow the `Pylons Unit Testing Guidelines +`_. +We'll give some additional, CKAN-specific guidelines below: + + +Naming test methods +------------------- + +Some modules in CKAN contain large numbers of more-or-less unrelated functions. +For example, :mod:`ckan.logic.action.update` contains all functions for +updating things in CKAN. This means that +:mod:`ckan.tests.logic.action.test_update` is going to contain an even larger +number of test functions. + +So in addition to the name of each test function clearly explaining the intent +of the test, it's important to name the test function after the function it's +testing, for example all the tests for ``user_update`` should be named +``test_user_update_*``: + +* ``test_user_update_name`` +* ``test_user_update_password`` +* ``test_user_update_with_id_that_does_not_exist`` +* etc. + +It's also a good idea to keep all the ``user_update`` tests next to each other +in the file, and to order the tests for each function in the same order as the +functions are ordered in the source file. + +In smaller modules putting the source function name in the test function names +may not be necessary, but for a lot of modules in CKAN it's probably a good +idea. + +:mod:`ckan.tests.helpers` and :mod:`ckan.tests.data` +---------------------------------------------------- + +.. todo:: + + There are some test helper functions here. Explain what they're for and + maybe autodoc them here. + + +:mod:`ckan.tests.logic.action` +------------------------------ + +Tests for action functions should use the +:func:`ckan.tests.helpers.call_action` function to call the action functions. + +One thing :func:`~ckan.tests.helpers.call_action` does is to add +``ignore_auth: True`` into the ``context`` dict that's passed to the action +function. This means CKAN will not call the action function's authorization +function. :mod:`ckan.tests.logic.action` should not test authorization +(e.g. testing that users that should not be authorized cannot call an action, +etc.) because the authorization functions are tested separately in +:mod:`ckan.tests.logic.auth`. + +Action function tests *should* test the logic of the actions themselves, and +*should* test validation (e.g. that various kinds of valid input work as +expected, and invalid inputs raise the expected exceptions). + +.. todo:: + + Insert some examples here. + + +:mod:`ckan.tests.controllers` +----------------------------- + +Tests for controller methods should work by simulating HTTP requests and +testing the HTML that they get back. + +In general the tests for a controller shouldn't need to be too detailed, +because there shouldn't be a lot of complicated logic and code in controller +classes (the logic should be handled in :mod:`ckan.logic` and :mod:`ckan.lib`, +for example). The tests for a controller should: + +* Make sure that the template renders without crashing. + +* Test that the page contents seem basically correct, or test certain important + elements in the page contents (but don't do too much HTML parsing). + +* Test that submitting any forms on the page works without crashing and has + the expected side-effects. + +When asserting side-effects after submitting a form, controller tests should +user the :func:`ckan.tests.helpers.call_action` function. For example after +creating a new user by submitting the new user form, a test could call the +:func:`~ckan.logic.action.get.user_show` action function to verify that the +user was created with the correct values. + +.. warning:: + + Some CKAN controllers *do* contain a lot of complicated logic code. These + controllers should be refactored to move the logic into :mod:`ckan.logic` or + :mod:`ckan.lib` where it can be tested easily. Unfortunately in cases like + this it may be necessary to write a lot of controller tests to get this + code's behavior into a test harness before it can be safely refactored. + +.. todo:: + + How exactly should controller tests work? (e.g. with a webtest testapp and + beautifulsoup?) + + Insert examples here. + + +Mocking +------- + +.. todo:: + + Some examples of how (and when/where) to use the ``mock`` library in CKAN. From dab8d49e6d9a89df4e940cbed4282ab5d7698576 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 25 Jul 2013 13:56:39 +0200 Subject: [PATCH 014/189] [#1117] Add a few new-style tests for user_update auth --- ckan/new_tests/logic/auth/test_update.py | 70 ++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 ckan/new_tests/logic/auth/test_update.py diff --git a/ckan/new_tests/logic/auth/test_update.py b/ckan/new_tests/logic/auth/test_update.py new file mode 100644 index 00000000000..49bf163178f --- /dev/null +++ b/ckan/new_tests/logic/auth/test_update.py @@ -0,0 +1,70 @@ +'''Unit tests for ckan/logic/auth.update.py. + +''' +import ckan.new_tests.helpers as helpers +import ckan.new_tests.data as data + + +class TestUpdate(object): + + @classmethod + def setup_class(cls): + helpers.reset_db() + + def setup(self): + import ckan.model as model + model.repo.rebuild_db() + + # TODO: Probably all auth function tests want this? Move to helpers.py? + def _call_auth(self, auth_name, context=None, **kwargs): + '''Call a ckan.logic.auth function more conveniently for testing. + + ''' + import ckan.model + import ckan.logic.auth.update + if context is None: + context = {} + context.setdefault('user', '127.0.0.1') + + context.setdefault('model', ckan.model) + + # FIXME: Do we want to go through check_access() here? + auth_function = ckan.logic.auth.update.__getattribute__(auth_name) + return auth_function(context=context, data_dict=kwargs) + + def test_user_update_visitor_cannot_update_user(self): + '''Visitors should not be able to update users' accounts.''' + + user = helpers.call_action('user_create', **data.typical_user()) + user['name'] = 'updated' + + # Try to update the user, but without passing any API key. + result = self._call_auth('user_update', **user) + assert result['success'] is False + + # TODO: Assert result['msg'] as well? In this case the message is not + # very sensible: "User 127.0.0.1 is not authorized to edit user foo". + # Also applies to the rest of these tests. + + def test_user_update_user_cannot_update_another_user(self): + '''Users should not be able to update other users' accounts.''' + + fred = helpers.call_action('user_create', **data.typical_user()) + bob = helpers.call_action('user_create', name='bob', + email='bob@bob.com', password='pass') + fred['name'] = 'updated' + + # Make Bob try to update Fred's user account. + context = {'user': bob['name']} + result = self._call_auth('user_update', context=context, **fred) + assert result['success'] is False + + def test_user_update_user_can_update_herself(self): + '''Users should be authorized to update their own accounts.''' + + user = helpers.call_action('user_create', **data.typical_user()) + + context = {'user': user['name']} + user['name'] = 'updated' + result = self._call_auth('user_update', context=context, **user) + assert result['success'] is True From 19b0b016d5acb3162bb7fbaf40fc9c4320f384e8 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 25 Jul 2013 15:25:55 +0200 Subject: [PATCH 015/189] [#1117] Never return reset_keys in user dicts Like the user's password, there's never any reason for a user dict to contain their reset key (even if the user dict is being generated for the user herself or for a sysadmin) --- ckan/lib/dictization/model_dictize.py | 1 + ckan/tests/lib/test_dictization.py | 8 ++++---- ckan/tests/logic/test_action.py | 2 -- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/ckan/lib/dictization/model_dictize.py b/ckan/lib/dictization/model_dictize.py index 3bb20695820..9687c52ae98 100644 --- a/ckan/lib/dictization/model_dictize.py +++ b/ckan/lib/dictization/model_dictize.py @@ -455,6 +455,7 @@ def user_dictize(user, context): result_dict = d.table_dictize(user, context) del result_dict['password'] + del result_dict['reset_key'] result_dict['display_name'] = user.display_name result_dict['email_hash'] = user.email_hash diff --git a/ckan/tests/lib/test_dictization.py b/ckan/tests/lib/test_dictization.py index 04237766fdb..94059d94476 100644 --- a/ckan/tests/lib/test_dictization.py +++ b/ckan/tests/lib/test_dictization.py @@ -1136,11 +1136,11 @@ def test_22_user_dictize_as_sysadmin(self): # Check sensitive data is available assert 'apikey' in user_dict - assert 'reset_key' in user_dict assert 'email' in user_dict - # Passwords should never be available + # Passwords and reset keys should never be available assert 'password' not in user_dict + assert 'reset_key' not in user_dict def test_23_user_dictize_as_same_user(self): '''User should be able to see their own sensitive data.''' @@ -1160,11 +1160,11 @@ def test_23_user_dictize_as_same_user(self): # Check sensitive data is available assert 'apikey' in user_dict - assert 'reset_key' in user_dict assert 'email' in user_dict - # Passwords should never be available + # Passwords and reset keys should never be available assert 'password' not in user_dict + assert 'reset_key' not in user_dict def test_24_user_dictize_as_other_user(self): '''User should not be able to see other's sensitive data.''' diff --git a/ckan/tests/logic/test_action.py b/ckan/tests/logic/test_action.py index ddf9d3576a3..41e9de47e62 100644 --- a/ckan/tests/logic/test_action.py +++ b/ckan/tests/logic/test_action.py @@ -370,7 +370,6 @@ def test_05_user_show(self): result = res_obj['result'] assert result['name'] == 'annafan' assert 'apikey' in result - assert 'reset_key' in result # Sysadmin user can see everyone's api key res = self.app.post('/api/action/user_show', params=postparams, @@ -380,7 +379,6 @@ def test_05_user_show(self): result = res_obj['result'] assert result['name'] == 'annafan' assert 'apikey' in result - assert 'reset_key' in result def test_05_user_show_edits(self): postparams = '%s=1' % json.dumps({'id':'tester'}) From a956b1f7de469cf89a36441c2b11d12bb5fadcc7 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 25 Jul 2013 16:11:30 +0200 Subject: [PATCH 016/189] [#1117] Add docstring and unit tests for ignore_missing validator --- ckan/lib/navl/validators.py | 13 +++++ ckan/new_tests/lib/navl/test_validators.py | 65 ++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 ckan/new_tests/lib/navl/test_validators.py diff --git a/ckan/lib/navl/validators.py b/ckan/lib/navl/validators.py index 01cbd87567f..b7ac124e624 100644 --- a/ckan/lib/navl/validators.py +++ b/ckan/lib/navl/validators.py @@ -76,7 +76,20 @@ def callable(key, data, errors, context): return callable def ignore_missing(key, data, errors, context): + '''If the key is missing from the data, ignore the rest of the key's + schema. + By putting ignore_missing at the start of the schema list for a key, + you can allow users to post a dict without the key and the dict will pass + validation. But if they post a dict that does contain the key, then any + validators after ignore_missing in the key's schema list will be applied. + + :raises ckan.lib.navl.dictization_functions.StopOnError: if ``data[key]`` + is :py:data:`ckan.lib.navl.dictization_functions.missing` or ``None`` + + :returns: ``None`` + + ''' value = data.get(key) if value is missing or value is None: diff --git a/ckan/new_tests/lib/navl/test_validators.py b/ckan/new_tests/lib/navl/test_validators.py new file mode 100644 index 00000000000..a3eaf3a5ee5 --- /dev/null +++ b/ckan/new_tests/lib/navl/test_validators.py @@ -0,0 +1,65 @@ +'''Unit tests for ckan/lib/navl/validators.py. + +''' +import nose.tools + + +class TestValidators(object): + + def test_ignore_missing_with_None(self): + '''If data[key] is None ignore_missing() should raise StopOnError.''' + + import ckan.lib.navl.dictization_functions as df + import ckan.lib.navl.validators as validators + + key = ('foo',) + data = {key: None} + errors = {key: []} + + with nose.tools.assert_raises(df.StopOnError): + validators.ignore_missing( + key=key, + data=data, + errors=errors, + context={}) + + # ignore_missing should remove the item from the dict. + assert key not in data + + def test_ignore_missing_with_missing(self): + '''If data[key] is missing ignore_missing() should raise StopOnError. + + ''' + import ckan.lib.navl.dictization_functions as df + import ckan.lib.navl.validators as validators + + key = ('foo',) + data = {key: df.missing} + errors = {key: []} + + with nose.tools.assert_raises(df.StopOnError): + validators.ignore_missing( + key=key, + data=data, + errors=errors, + context={}) + + # ignore_missing should remove the item from the dict. + assert key not in data + + def test_ignore_missing_with_a_value(self): + '''If data[key] is neither None or missing, ignore_missing() should do + nothing. + + ''' + import ckan.lib.navl.validators as validators + + key = ('foo',) + data = {key: 'bar'} + errors = {key: []} + + result = validators.ignore_missing(key=key, data=data, errors=errors, + context={}) + + assert result is None + assert data.get(key) == 'bar' From ca16b016287d013f41abd033cb95085762ac96ca Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 25 Jul 2013 16:15:50 +0200 Subject: [PATCH 017/189] [#1117] Add another ignore_missing test --- ckan/new_tests/lib/navl/test_validators.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/ckan/new_tests/lib/navl/test_validators.py b/ckan/new_tests/lib/navl/test_validators.py index a3eaf3a5ee5..1d0f526b273 100644 --- a/ckan/new_tests/lib/navl/test_validators.py +++ b/ckan/new_tests/lib/navl/test_validators.py @@ -47,6 +47,26 @@ def test_ignore_missing_with_missing(self): # ignore_missing should remove the item from the dict. assert key not in data + def test_ignore_missing_without_key(self): + '''If key is not in data ignore_missing() should raise StopOnError.''' + + import ckan.lib.navl.dictization_functions as df + import ckan.lib.navl.validators as validators + + key = ('foo',) + data = {} + errors = {key: []} + + with nose.tools.assert_raises(df.StopOnError): + validators.ignore_missing( + key=key, + data=data, + errors=errors, + context={}) + + # ignore_missing should remove the item from the dict. + assert key not in data + def test_ignore_missing_with_a_value(self): '''If data[key] is neither None or missing, ignore_missing() should do nothing. From 0365a9599e3050256879f982d5edeade1d0f3a83 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 25 Jul 2013 16:17:47 +0200 Subject: [PATCH 018/189] [#1117] Add some asserts to ignore_missing tests --- ckan/new_tests/lib/navl/test_validators.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ckan/new_tests/lib/navl/test_validators.py b/ckan/new_tests/lib/navl/test_validators.py index 1d0f526b273..1e34b51face 100644 --- a/ckan/new_tests/lib/navl/test_validators.py +++ b/ckan/new_tests/lib/navl/test_validators.py @@ -26,6 +26,9 @@ def test_ignore_missing_with_None(self): # ignore_missing should remove the item from the dict. assert key not in data + # ignore_missing should not add any errors. + assert errors[key] == [] + def test_ignore_missing_with_missing(self): '''If data[key] is missing ignore_missing() should raise StopOnError. @@ -47,6 +50,9 @@ def test_ignore_missing_with_missing(self): # ignore_missing should remove the item from the dict. assert key not in data + # ignore_missing should not add any errors. + assert errors[key] == [] + def test_ignore_missing_without_key(self): '''If key is not in data ignore_missing() should raise StopOnError.''' @@ -67,6 +73,9 @@ def test_ignore_missing_without_key(self): # ignore_missing should remove the item from the dict. assert key not in data + # ignore_missing should not add any errors. + assert errors[key] == [] + def test_ignore_missing_with_a_value(self): '''If data[key] is neither None or missing, ignore_missing() should do nothing. @@ -82,4 +91,9 @@ def test_ignore_missing_with_a_value(self): context={}) assert result is None + + # ignore_missing shouldn't remove or modify the value. assert data.get(key) == 'bar' + + # ignore_missing shouldn't add any errors. + assert errors[key] == [] From 262adf316d0ac4131341fbd07e4cade16531d36d Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 25 Jul 2013 16:24:10 +0200 Subject: [PATCH 019/189] [#1117] Tweak a unit test --- ckan/new_tests/lib/navl/test_validators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ckan/new_tests/lib/navl/test_validators.py b/ckan/new_tests/lib/navl/test_validators.py index 1e34b51face..0b38b7e3a91 100644 --- a/ckan/new_tests/lib/navl/test_validators.py +++ b/ckan/new_tests/lib/navl/test_validators.py @@ -70,8 +70,8 @@ def test_ignore_missing_without_key(self): errors=errors, context={}) - # ignore_missing should remove the item from the dict. - assert key not in data + # ignore_missing shouldn't change the data dict. + assert data == {} # ignore_missing should not add any errors. assert errors[key] == [] From 69ea1ad0ca2c0d9a4ee1faf95c25e28b64d4cfe6 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 25 Jul 2013 17:10:00 +0200 Subject: [PATCH 020/189] [#1117] Combine three unit tests into one --- ckan/new_tests/lib/navl/test_validators.py | 88 ++++++---------------- 1 file changed, 23 insertions(+), 65 deletions(-) diff --git a/ckan/new_tests/lib/navl/test_validators.py b/ckan/new_tests/lib/navl/test_validators.py index 0b38b7e3a91..9a3dc4e7256 100644 --- a/ckan/new_tests/lib/navl/test_validators.py +++ b/ckan/new_tests/lib/navl/test_validators.py @@ -6,75 +6,33 @@ class TestValidators(object): - def test_ignore_missing_with_None(self): - '''If data[key] is None ignore_missing() should raise StopOnError.''' + def test_ignore_missing_with_value_missing(self): + '''If data[key] is None or dictization_functions.missing or if + key is not in data, then ignore_missing() should raise StopOnError.''' import ckan.lib.navl.dictization_functions as df import ckan.lib.navl.validators as validators - key = ('foo',) - data = {key: None} - errors = {key: []} - - with nose.tools.assert_raises(df.StopOnError): - validators.ignore_missing( - key=key, - data=data, - errors=errors, - context={}) - - # ignore_missing should remove the item from the dict. - assert key not in data - - # ignore_missing should not add any errors. - assert errors[key] == [] - - def test_ignore_missing_with_missing(self): - '''If data[key] is missing ignore_missing() should raise StopOnError. - - ''' - import ckan.lib.navl.dictization_functions as df - import ckan.lib.navl.validators as validators - - key = ('foo',) - data = {key: df.missing} - errors = {key: []} - - with nose.tools.assert_raises(df.StopOnError): - validators.ignore_missing( - key=key, - data=data, - errors=errors, - context={}) - - # ignore_missing should remove the item from the dict. - assert key not in data - - # ignore_missing should not add any errors. - assert errors[key] == [] - - def test_ignore_missing_without_key(self): - '''If key is not in data ignore_missing() should raise StopOnError.''' - - import ckan.lib.navl.dictization_functions as df - import ckan.lib.navl.validators as validators - - key = ('foo',) - data = {} - errors = {key: []} - - with nose.tools.assert_raises(df.StopOnError): - validators.ignore_missing( - key=key, - data=data, - errors=errors, - context={}) - - # ignore_missing shouldn't change the data dict. - assert data == {} - - # ignore_missing should not add any errors. - assert errors[key] == [] + for value in (None, df.missing, 'skip'): + key = ('foo',) + if value == 'skip': + data = {} + else: + data = {key: value} + errors = {key: []} + + with nose.tools.assert_raises(df.StopOnError): + validators.ignore_missing( + key=key, + data=data, + errors=errors, + context={}) + + # ignore_missing should remove the item from the dict. + assert key not in data + + # ignore_missing should not add any errors. + assert errors[key] == [] def test_ignore_missing_with_a_value(self): '''If data[key] is neither None or missing, ignore_missing() should do From e7d5c9d08ddf3153c08ee3e96ee8e6b956db7523 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 25 Jul 2013 17:26:10 +0200 Subject: [PATCH 021/189] [#1117] Improve a unit test Add some more realistic data and check that it doesn't get modified. --- ckan/new_tests/lib/navl/test_validators.py | 28 ++++++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/ckan/new_tests/lib/navl/test_validators.py b/ckan/new_tests/lib/navl/test_validators.py index 9a3dc4e7256..cf1334adbcc 100644 --- a/ckan/new_tests/lib/navl/test_validators.py +++ b/ckan/new_tests/lib/navl/test_validators.py @@ -14,13 +14,30 @@ def test_ignore_missing_with_value_missing(self): import ckan.lib.navl.validators as validators for value in (None, df.missing, 'skip'): - key = ('foo',) + + # This is the key for the value that is going to be validated. + key = ('key to be validated',) + + # This is another random key that's going to be in the data and + # errors dict just so we can test that ignore_missing() doesn't + # modify it. + other_key = ('other key',) + if value == 'skip': data = {} else: data = {key: value} + + # Add some other random stuff into data, just so we can test that + # ignore_missing() doesn't modify it. + data[other_key] = 'other value' + errors = {key: []} + # Add some other random errors into errors, just so we can test + # that ignore_missing doesn't modify them. + errors[other_key] = ['other error'] + with nose.tools.assert_raises(df.StopOnError): validators.ignore_missing( key=key, @@ -28,11 +45,12 @@ def test_ignore_missing_with_value_missing(self): errors=errors, context={}) - # ignore_missing should remove the item from the dict. - assert key not in data + # ignore_missing should remove the key being validated from the + # dict, but it should not remove other keys or add any keys. + assert data == {other_key: 'other value'} - # ignore_missing should not add any errors. - assert errors[key] == [] + # ignore_missing shouldn't modify the errors dict. + assert errors == {key: [], other_key: ['other error']} def test_ignore_missing_with_a_value(self): '''If data[key] is neither None or missing, ignore_missing() should do From 476b8ff944eeab40f6c98a5334df69b6d6787332 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 25 Jul 2013 17:41:33 +0200 Subject: [PATCH 022/189] [#1117] Move some unit test code into helper functions --- ckan/new_tests/lib/navl/test_validators.py | 65 ++++++++++++++-------- 1 file changed, 43 insertions(+), 22 deletions(-) diff --git a/ckan/new_tests/lib/navl/test_validators.py b/ckan/new_tests/lib/navl/test_validators.py index cf1334adbcc..b8307e54db4 100644 --- a/ckan/new_tests/lib/navl/test_validators.py +++ b/ckan/new_tests/lib/navl/test_validators.py @@ -1,9 +1,35 @@ '''Unit tests for ckan/lib/navl/validators.py. ''' +import copy + import nose.tools +def _data(): + '''Return a data dict with some random data in it, suitable to be passed + to validators for testing. + + This is a function that returns a dict (rather than just a dict as a + module-level variable) so that if one test method modifies the dict the + next test method gets a new clean copy. + + ''' + return {('other key',): 'other value'} + + +def _errors(): + '''Return an errors dict with some random errors in it, suitable to be + passed to validators for testing. + + This is a function that returns a dict (rather than just a dict as a + module-level variable) so that if one test method modifies the dict the + next test method gets a new clean copy. + + ''' + return {('other key',): ['other error']} + + class TestValidators(object): def test_ignore_missing_with_value_missing(self): @@ -18,25 +44,18 @@ def test_ignore_missing_with_value_missing(self): # This is the key for the value that is going to be validated. key = ('key to be validated',) - # This is another random key that's going to be in the data and - # errors dict just so we can test that ignore_missing() doesn't - # modify it. - other_key = ('other key',) - - if value == 'skip': - data = {} - else: - data = {key: value} - - # Add some other random stuff into data, just so we can test that - # ignore_missing() doesn't modify it. - data[other_key] = 'other value' + # The data to pass to the validator function for validation. + data = _data() + if value != 'skip': + data[key] = value - errors = {key: []} + # The errors dict to pass to the validator function. + errors = _errors() + errors[key] = [] - # Add some other random errors into errors, just so we can test - # that ignore_missing doesn't modify them. - errors[other_key] = ['other error'] + # Make copies of the data and errors dicts for asserting later. + original_data = copy.deepcopy(data) + original_errors = copy.deepcopy(errors) with nose.tools.assert_raises(df.StopOnError): validators.ignore_missing( @@ -45,12 +64,14 @@ def test_ignore_missing_with_value_missing(self): errors=errors, context={}) - # ignore_missing should remove the key being validated from the - # dict, but it should not remove other keys or add any keys. - assert data == {other_key: 'other value'} + # ignore_missing() should remove the key being validated from the + # data dict, but it should not remove other keys or add any keys. + if key in original_data: + del original_data[key] + assert data == original_data - # ignore_missing shouldn't modify the errors dict. - assert errors == {key: [], other_key: ['other error']} + # ignore_missing() shouldn't modify the errors dict. + assert errors == original_errors def test_ignore_missing_with_a_value(self): '''If data[key] is neither None or missing, ignore_missing() should do From 854407ae96b0722a874dcceb14192b28eb7ffe1e Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 25 Jul 2013 17:46:05 +0200 Subject: [PATCH 023/189] [#1117] Tweak a couple of docstrings --- ckan/new_tests/lib/navl/test_validators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ckan/new_tests/lib/navl/test_validators.py b/ckan/new_tests/lib/navl/test_validators.py index b8307e54db4..d97213752f5 100644 --- a/ckan/new_tests/lib/navl/test_validators.py +++ b/ckan/new_tests/lib/navl/test_validators.py @@ -7,7 +7,7 @@ def _data(): - '''Return a data dict with some random data in it, suitable to be passed + '''Return a data dict with some arbitrary data in it, suitable to be passed to validators for testing. This is a function that returns a dict (rather than just a dict as a @@ -19,7 +19,7 @@ def _data(): def _errors(): - '''Return an errors dict with some random errors in it, suitable to be + '''Return an errors dict with some arbitrary errors in it, suitable to be passed to validators for testing. This is a function that returns a dict (rather than just a dict as a From ed12fa0f61a555baa33ef762085df2b214dd46dc Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 25 Jul 2013 18:24:22 +0200 Subject: [PATCH 024/189] [#1117] Move some test comments into asserts This makes the output when tests fail more useful Unfortunately I don't see how to do this with assert_raises. --- ckan/new_tests/lib/navl/test_validators.py | 24 ++++++++++++---------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/ckan/new_tests/lib/navl/test_validators.py b/ckan/new_tests/lib/navl/test_validators.py index d97213752f5..6428b3a2358 100644 --- a/ckan/new_tests/lib/navl/test_validators.py +++ b/ckan/new_tests/lib/navl/test_validators.py @@ -58,20 +58,22 @@ def test_ignore_missing_with_value_missing(self): original_errors = copy.deepcopy(errors) with nose.tools.assert_raises(df.StopOnError): - validators.ignore_missing( - key=key, - data=data, - errors=errors, - context={}) - - # ignore_missing() should remove the key being validated from the - # data dict, but it should not remove other keys or add any keys. + validators.ignore_missing(key=key, data=data, errors=errors, + context={}) + + assert key not in data, ('When given a value of {value} ' + 'ignore_missing() should remove the item from the data ' + 'dict'.format(value=value)) + if key in original_data: del original_data[key] - assert data == original_data + assert data == original_data, ('When given a value of {value} ' + 'ignore_missing() should not modify other items in the ' + 'data dict'.format(value=value)) - # ignore_missing() shouldn't modify the errors dict. - assert errors == original_errors + assert errors == original_errors, ('When given a value of {value} ' + 'ignore_missing should not modify the errors dict'.format( + value=value)) def test_ignore_missing_with_a_value(self): '''If data[key] is neither None or missing, ignore_missing() should do From 2f8e74a0e2bb39c2fec1bf9604ab6ada764c1f2b Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Fri, 26 Jul 2013 12:15:42 +0200 Subject: [PATCH 025/189] [#1117] Tweak a docstring This is easier to read I think --- ckan/new_tests/lib/navl/test_validators.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ckan/new_tests/lib/navl/test_validators.py b/ckan/new_tests/lib/navl/test_validators.py index 6428b3a2358..3f00ef62330 100644 --- a/ckan/new_tests/lib/navl/test_validators.py +++ b/ckan/new_tests/lib/navl/test_validators.py @@ -33,9 +33,13 @@ def _errors(): class TestValidators(object): def test_ignore_missing_with_value_missing(self): - '''If data[key] is None or dictization_functions.missing or if - key is not in data, then ignore_missing() should raise StopOnError.''' + '''ignore_missing() should raise StopOnError if: + - data[key] is None, or + - data[key] is dictization_functions.missing, or + - key is not in data + + ''' import ckan.lib.navl.dictization_functions as df import ckan.lib.navl.validators as validators From e5d37e1d6543522aebd58ec9bcd1ef825179d0d2 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Fri, 26 Jul 2013 12:23:20 +0200 Subject: [PATCH 026/189] [#1117] Make a unit test more realistic Make it use the _data() and _errors() helper functions to get more realistic (non-empty) data and errors dicts. This just makes the test a little more thorough and realistic. --- ckan/new_tests/lib/navl/test_validators.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/ckan/new_tests/lib/navl/test_validators.py b/ckan/new_tests/lib/navl/test_validators.py index 3f00ef62330..a6d7d627b65 100644 --- a/ckan/new_tests/lib/navl/test_validators.py +++ b/ckan/new_tests/lib/navl/test_validators.py @@ -86,17 +86,23 @@ def test_ignore_missing_with_a_value(self): ''' import ckan.lib.navl.validators as validators - key = ('foo',) - data = {key: 'bar'} - errors = {key: []} + key = ('key to be validated',) + data = _data() + data[key] = 'value to be validated' + errors = _errors() + errors[key] = [] + + # Make copies of the data and errors dicts for asserting later. + original_data = copy.deepcopy(data) + original_errors = copy.deepcopy(errors) result = validators.ignore_missing(key=key, data=data, errors=errors, context={}) assert result is None - # ignore_missing shouldn't remove or modify the value. - assert data.get(key) == 'bar' + assert data == original_data, ("ignore_missing() shouldn't modify the " + "data dict") - # ignore_missing shouldn't add any errors. - assert errors[key] == [] + assert errors == original_errors, ("ignore_missing() shouldn't modify " + "the errors dict") From 404f06e778c6b9f2de8dea44c241769a598f2e7a Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Fri, 26 Jul 2013 13:57:18 +0200 Subject: [PATCH 027/189] [#1117] Add new tests and docstring for name_validator() There are no existing tests for this that I can see. --- ckan/logic/validators.py | 30 ++++++-- ckan/new_tests/lib/navl/test_validators.py | 80 ++++++++++++++++++++++ 2 files changed, 103 insertions(+), 7 deletions(-) diff --git a/ckan/logic/validators.py b/ckan/logic/validators.py index 5874f89d81c..67a68d0424b 100644 --- a/ckan/logic/validators.py +++ b/ckan/logic/validators.py @@ -275,23 +275,39 @@ def extras_unicode_convert(extras, context): return extras name_match = re.compile('[a-z0-9_\-]*$') -def name_validator(val, context): - if not isinstance(val, basestring): +def name_validator(value, context): + '''Return the given value if it's a valid name, otherwise raise Invalid. + + If it's a valid name, the given value will be returned unmodified. + + This function applies general validation rules for names of packages, + groups, users, etc. + + Most schemas also have their own custom name validator function to apply + custom validation rules after this function, for example a + ``package_name_validator()`` to check that no package with the given name + already exists. + + :raises ckan.lib.navl.dictization_functions.Invalid: if ``value`` is not + a valid name + + ''' + if not isinstance(value, basestring): raise Invalid(_('Names must be strings')) # check basic textual rules - if val in ['new', 'edit', 'search']: + if value in ['new', 'edit', 'search']: raise Invalid(_('That name cannot be used')) - if len(val) < 2: + if len(value) < 2: raise Invalid(_('Name must be at least %s characters long') % 2) - if len(val) > PACKAGE_NAME_MAX_LENGTH: + if len(value) > PACKAGE_NAME_MAX_LENGTH: raise Invalid(_('Name must be a maximum of %i characters long') % \ PACKAGE_NAME_MAX_LENGTH) - if not name_match.match(val): + if not name_match.match(value): raise Invalid(_('Url must be purely lowercase alphanumeric ' '(ascii) characters and these symbols: -_')) - return val + return value def package_name_validator(key, data, errors, context): model = context["model"] diff --git a/ckan/new_tests/lib/navl/test_validators.py b/ckan/new_tests/lib/navl/test_validators.py index a6d7d627b65..e40d9122c3a 100644 --- a/ckan/new_tests/lib/navl/test_validators.py +++ b/ckan/new_tests/lib/navl/test_validators.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- '''Unit tests for ckan/lib/navl/validators.py. ''' @@ -106,3 +107,82 @@ def test_ignore_missing_with_a_value(self): assert errors == original_errors, ("ignore_missing() shouldn't modify " "the errors dict") + + def test_name_validator_with_invalid_value(self): + '''If given an invalid value name_validator() should do raise Invalid. + + ''' + import ckan.logic.validators as validators + import ckan.lib.navl.dictization_functions as df + import ckan.model as model + + invalid_values = [ + # Non-string names aren't allowed as names. + 13, + 23.7, + 100L, + 1.0j, + None, + True, + False, + ('a', 2, False), + [13, None, True], + {'foo': 'bar'}, + lambda x: x**2, + + # Certain reserved strings aren't allowed as names. + 'new', + 'edit', + 'search', + + # Strings < 2 characters long aren't allowed as names. + '', + 'a', + '2', + + # Strings > PACKAGE_NAME_MAX_LENGTH long aren't allowed as names. + 'a' * (model.PACKAGE_NAME_MAX_LENGTH + 1), + + # Strings containing non-ascii characters aren't allowed as names. + u"fred_❤%'\"Ußabc@fred.com", + + # Strings containing upper-case characters aren't allowed as names. + 'seanH', + + # Strings containing spaces aren't allowed as names. + 'sean h', + + # Strings containing punctuation aren't allowed as names. + 'seanh!', + ] + + for invalid_value in invalid_values: + with nose.tools.assert_raises(df.Invalid): + validators.name_validator(invalid_value, context={}) + + def test_name_validator_with_valid_value(self): + '''If given a valid string name_validator() should do nothing and + return the string. + + ''' + import ckan.logic.validators as validators + import ckan.model as model + + valid_names = [ + 'fred', + 'fred-flintstone', + 'fred_flintstone', + 'fred_flintstone-9', + 'f' * model.PACKAGE_NAME_MAX_LENGTH, + '-' * model.PACKAGE_NAME_MAX_LENGTH, + '_' * model.PACKAGE_NAME_MAX_LENGTH, + '9' * model.PACKAGE_NAME_MAX_LENGTH, + '99', + '--', + '__', + ] + + for valid_name in valid_names: + result = validators.name_validator(valid_name, context={}) + assert result == valid_name, ('If given a valid string ' + 'name_validator() should return the string unmodified.') From a0a3d93ca87df5c2af629dde365fe872c4c248d9 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Fri, 26 Jul 2013 14:01:30 +0200 Subject: [PATCH 028/189] [#1117] Add a valid unicode name to name_validator() test --- ckan/new_tests/lib/navl/test_validators.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ckan/new_tests/lib/navl/test_validators.py b/ckan/new_tests/lib/navl/test_validators.py index e40d9122c3a..191db6877bb 100644 --- a/ckan/new_tests/lib/navl/test_validators.py +++ b/ckan/new_tests/lib/navl/test_validators.py @@ -180,6 +180,7 @@ def test_name_validator_with_valid_value(self): '99', '--', '__', + u'fred-flintstone_9', ] for valid_name in valid_names: From f3d0de6208486528f0e08f7edcbba70faea2dd86 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Fri, 26 Jul 2013 16:31:16 +0200 Subject: [PATCH 029/189] [#1117] Move some validator tests into the right test module Oops :) --- ckan/new_tests/lib/navl/test_validators.py | 80 -------------------- ckan/new_tests/logic/test_validators.py | 88 ++++++++++++++++++++++ 2 files changed, 88 insertions(+), 80 deletions(-) create mode 100644 ckan/new_tests/logic/test_validators.py diff --git a/ckan/new_tests/lib/navl/test_validators.py b/ckan/new_tests/lib/navl/test_validators.py index 191db6877bb..0405678ef1e 100644 --- a/ckan/new_tests/lib/navl/test_validators.py +++ b/ckan/new_tests/lib/navl/test_validators.py @@ -107,83 +107,3 @@ def test_ignore_missing_with_a_value(self): assert errors == original_errors, ("ignore_missing() shouldn't modify " "the errors dict") - - def test_name_validator_with_invalid_value(self): - '''If given an invalid value name_validator() should do raise Invalid. - - ''' - import ckan.logic.validators as validators - import ckan.lib.navl.dictization_functions as df - import ckan.model as model - - invalid_values = [ - # Non-string names aren't allowed as names. - 13, - 23.7, - 100L, - 1.0j, - None, - True, - False, - ('a', 2, False), - [13, None, True], - {'foo': 'bar'}, - lambda x: x**2, - - # Certain reserved strings aren't allowed as names. - 'new', - 'edit', - 'search', - - # Strings < 2 characters long aren't allowed as names. - '', - 'a', - '2', - - # Strings > PACKAGE_NAME_MAX_LENGTH long aren't allowed as names. - 'a' * (model.PACKAGE_NAME_MAX_LENGTH + 1), - - # Strings containing non-ascii characters aren't allowed as names. - u"fred_❤%'\"Ußabc@fred.com", - - # Strings containing upper-case characters aren't allowed as names. - 'seanH', - - # Strings containing spaces aren't allowed as names. - 'sean h', - - # Strings containing punctuation aren't allowed as names. - 'seanh!', - ] - - for invalid_value in invalid_values: - with nose.tools.assert_raises(df.Invalid): - validators.name_validator(invalid_value, context={}) - - def test_name_validator_with_valid_value(self): - '''If given a valid string name_validator() should do nothing and - return the string. - - ''' - import ckan.logic.validators as validators - import ckan.model as model - - valid_names = [ - 'fred', - 'fred-flintstone', - 'fred_flintstone', - 'fred_flintstone-9', - 'f' * model.PACKAGE_NAME_MAX_LENGTH, - '-' * model.PACKAGE_NAME_MAX_LENGTH, - '_' * model.PACKAGE_NAME_MAX_LENGTH, - '9' * model.PACKAGE_NAME_MAX_LENGTH, - '99', - '--', - '__', - u'fred-flintstone_9', - ] - - for valid_name in valid_names: - result = validators.name_validator(valid_name, context={}) - assert result == valid_name, ('If given a valid string ' - 'name_validator() should return the string unmodified.') diff --git a/ckan/new_tests/logic/test_validators.py b/ckan/new_tests/logic/test_validators.py new file mode 100644 index 00000000000..64cee386440 --- /dev/null +++ b/ckan/new_tests/logic/test_validators.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +'''Unit tests for ckan/logic/validators.py. + +''' +import nose.tools + + +class TestValidators(object): + + def test_name_validator_with_invalid_value(self): + '''If given an invalid value name_validator() should do raise Invalid. + + ''' + import ckan.logic.validators as validators + import ckan.lib.navl.dictization_functions as df + import ckan.model as model + + invalid_values = [ + # Non-string names aren't allowed as names. + 13, + 23.7, + 100L, + 1.0j, + None, + True, + False, + ('a', 2, False), + [13, None, True], + {'foo': 'bar'}, + lambda x: x**2, + + # Certain reserved strings aren't allowed as names. + 'new', + 'edit', + 'search', + + # Strings < 2 characters long aren't allowed as names. + '', + 'a', + '2', + + # Strings > PACKAGE_NAME_MAX_LENGTH long aren't allowed as names. + 'a' * (model.PACKAGE_NAME_MAX_LENGTH + 1), + + # Strings containing non-ascii characters aren't allowed as names. + u"fred_❤%'\"Ußabc@fred.com", + + # Strings containing upper-case characters aren't allowed as names. + 'seanH', + + # Strings containing spaces aren't allowed as names. + 'sean h', + + # Strings containing punctuation aren't allowed as names. + 'seanh!', + ] + + for invalid_value in invalid_values: + with nose.tools.assert_raises(df.Invalid): + validators.name_validator(invalid_value, context={}) + + def test_name_validator_with_valid_value(self): + '''If given a valid string name_validator() should do nothing and + return the string. + + ''' + import ckan.logic.validators as validators + import ckan.model as model + + valid_names = [ + 'fred', + 'fred-flintstone', + 'fred_flintstone', + 'fred_flintstone-9', + 'f' * model.PACKAGE_NAME_MAX_LENGTH, + '-' * model.PACKAGE_NAME_MAX_LENGTH, + '_' * model.PACKAGE_NAME_MAX_LENGTH, + '9' * model.PACKAGE_NAME_MAX_LENGTH, + '99', + '--', + '__', + u'fred-flintstone_9', + ] + + for valid_name in valid_names: + result = validators.name_validator(valid_name, context={}) + assert result == valid_name, ('If given a valid string ' + 'name_validator() should return the string unmodified.') From f234745c5f034ce59fb919dcf5536688a664bb88 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Fri, 26 Jul 2013 17:21:20 +0200 Subject: [PATCH 030/189] [#1117] Move some helpers into shared test helper data module --- ckan/new_tests/data.py | 16 +++++++++++ ckan/new_tests/lib/navl/test_validators.py | 32 ++++------------------ 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/ckan/new_tests/data.py b/ckan/new_tests/data.py index e7e9f120e1c..c8b534a2a3c 100644 --- a/ckan/new_tests/data.py +++ b/ckan/new_tests/data.py @@ -12,3 +12,19 @@ def typical_user(): 'email': 'fred@fred.com', 'password': 'wilma', } + + +def validator_data_dict(): + '''Return a data dict with some arbitrary data in it, suitable to be passed + to validator functions for testing. + + ''' + return {('other key',): 'other value'} + + +def validator_errors_dict(): + '''Return an errors dict with some arbitrary errors in it, suitable to be + passed to validator functions for testing. + + ''' + return {('other key',): ['other error']} diff --git a/ckan/new_tests/lib/navl/test_validators.py b/ckan/new_tests/lib/navl/test_validators.py index 0405678ef1e..51df87da648 100644 --- a/ckan/new_tests/lib/navl/test_validators.py +++ b/ckan/new_tests/lib/navl/test_validators.py @@ -6,29 +6,7 @@ import nose.tools - -def _data(): - '''Return a data dict with some arbitrary data in it, suitable to be passed - to validators for testing. - - This is a function that returns a dict (rather than just a dict as a - module-level variable) so that if one test method modifies the dict the - next test method gets a new clean copy. - - ''' - return {('other key',): 'other value'} - - -def _errors(): - '''Return an errors dict with some arbitrary errors in it, suitable to be - passed to validators for testing. - - This is a function that returns a dict (rather than just a dict as a - module-level variable) so that if one test method modifies the dict the - next test method gets a new clean copy. - - ''' - return {('other key',): ['other error']} +import ckan.new_tests.data as test_data class TestValidators(object): @@ -50,12 +28,12 @@ def test_ignore_missing_with_value_missing(self): key = ('key to be validated',) # The data to pass to the validator function for validation. - data = _data() + data = test_data.validator_data_dict() if value != 'skip': data[key] = value # The errors dict to pass to the validator function. - errors = _errors() + errors = test_data.validator_errors_dict() errors[key] = [] # Make copies of the data and errors dicts for asserting later. @@ -88,9 +66,9 @@ def test_ignore_missing_with_a_value(self): import ckan.lib.navl.validators as validators key = ('key to be validated',) - data = _data() + data = test_data.validator_data_dict() data[key] = 'value to be validated' - errors = _errors() + errors = test_data.validator_errors_dict() errors[key] = [] # Make copies of the data and errors dicts for asserting later. From a3485a1fa4a872abc3d29f382069f34785f72f1e Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Fri, 26 Jul 2013 17:51:03 +0200 Subject: [PATCH 031/189] [#1117] Add unit tests for user_name_validator() --- ckan/new_tests/logic/test_validators.py | 142 ++++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/ckan/new_tests/logic/test_validators.py b/ckan/new_tests/logic/test_validators.py index 64cee386440..d6556532347 100644 --- a/ckan/new_tests/logic/test_validators.py +++ b/ckan/new_tests/logic/test_validators.py @@ -2,11 +2,27 @@ '''Unit tests for ckan/logic/validators.py. ''' +import copy + import nose.tools +import ckan.new_tests.helpers as helpers +import ckan.new_tests.data as test_data + class TestValidators(object): + @classmethod + def setup_class(cls): + # Initialize the test db (if it isn't already) and clean out any data + # left in it. + helpers.reset_db() + + def setup(self): + import ckan.model as model + # Reset the db before each test method. + model.repo.rebuild_db() + def test_name_validator_with_invalid_value(self): '''If given an invalid value name_validator() should do raise Invalid. @@ -86,3 +102,129 @@ def test_name_validator_with_valid_value(self): result = validators.name_validator(valid_name, context={}) assert result == valid_name, ('If given a valid string ' 'name_validator() should return the string unmodified.') + + def test_user_name_validator_with_non_string_value(self): + '''user_name_validator() should raise Invalid if given a non-string + value. + + ''' + import ckan.logic.validators as validators + import ckan.model as model + import ckan.lib.navl.dictization_functions as df + + non_string_values = [ + 13, + 23.7, + 100L, + 1.0j, + None, + True, + False, + ('a', 2, False), + [13, None, True], + {'foo': 'bar'}, + lambda x: x**2, + ] + + key = ('name',) + context = { + 'model': model, + 'session': model.Session, + 'user': '', + } + + for non_string_value in non_string_values: + data = test_data.validator_data_dict() + data[key] = non_string_value + errors = test_data.validator_errors_dict() + errors[key] = [] + + # Make copies of the data and errors dicts for asserting later. + original_data = copy.deepcopy(data) + original_errors = copy.deepcopy(errors) + + with nose.tools.assert_raises(df.Invalid): + validators.user_name_validator(key, data, errors, context) + + assert data == original_data, ("user_name_validator shouldn't " + 'modify the data dict') + + assert errors == original_errors, ("user_name_validator shouldn't " + 'modify the errors dict') + + def test_user_name_validator_with_a_name_that_already_exists(self): + '''user_name_validator() should add to the errors dict if given a + user name that already exists. + + ''' + import ckan.logic.validators as validators + import ckan.model as model + + existing_user = helpers.call_action('user_create', + **test_data.typical_user()) + + # Try to create another user with the same name as the existing user. + data = test_data.validator_data_dict() + key = ('name',) + data[key] = existing_user['name'] + errors = test_data.validator_errors_dict() + errors[key] = [] + context = { + 'model': model, + 'session': model.Session, + 'user': '', + } + + # Make copies of the data and errors dicts for asserting later. + original_data = copy.deepcopy(data) + original_errors = copy.deepcopy(errors) + + result = validators.user_name_validator(key, data, errors, context) + + assert result is None, ("user_name_validator() shouldn't return " + "anything") + + msg = 'That login name is not available.' + assert errors[key] == [msg], ('user_name_validator() should add to ' + 'the errors dict when given the name of ' + 'a user that already exists') + + errors[key] = [] + assert errors == original_errors, ('user_name_validator() should not ' + 'modify other parts of the errors ' + 'dict') + + assert data == original_data, ('user_name_validator() should not ' + 'modify the data dict') + + def test_user_name_validator_successful(self): + '''user_name_validator() should do nothing if given a valid name.''' + + import ckan.logic.validators as validators + import ckan.model as model + + data = test_data.validator_data_dict() + key = ('name',) + data[key] = 'new_user_name' + errors = test_data.validator_errors_dict() + errors[key] = [] + context = { + 'model': model, + 'session': model.Session, + 'user': '', + } + + # Make copies of the data and errors dicts for asserting later. + original_data = copy.deepcopy(data) + original_errors = copy.deepcopy(errors) + + result = validators.user_name_validator(key, data, errors, context) + + assert result is None, ("user_name_validator() shouldn't return " + 'anything') + + assert data == original_data, ("user_name_validator shouldn't modify " + 'the data dict') + + assert errors == original_errors, ("user_name_validator shouldn't " + 'modify the errors dict') From 807b601f3ade09aac3f0ef0d530e947774e93975 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Fri, 26 Jul 2013 19:16:57 +0200 Subject: [PATCH 032/189] [#1117] Fix user_password validator Commit f3165ce (user_update: don't crash on non-string password) unwittingly changed the behavior of user_password_validator(), causing some legacy tests to fail: If the given password is Missing or '' user_password_validator() should *not* raise Invalid. Fiux user_password_validator to not raise Invalid in these cases, so that the tests pass again. --- ckan/logic/validators.py | 4 +++- ckan/new_tests/logic/action/test_update.py | 20 ++++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/ckan/logic/validators.py b/ckan/logic/validators.py index 67a68d0424b..a18cf783d64 100644 --- a/ckan/logic/validators.py +++ b/ckan/logic/validators.py @@ -524,9 +524,11 @@ def user_password_validator(key, data, errors, context): value = data[key] if isinstance(value, Missing): - errors[('password',)].append(_('Your password must be 4 characters or longer')) + pass elif not isinstance(value, basestring): errors[('password',)].append(_('Passwords must be strings')) + elif value == '': + pass elif len(value) < 4: errors[('password',)].append(_('Your password must be 4 characters or longer')) diff --git a/ckan/new_tests/logic/action/test_update.py b/ckan/new_tests/logic/action/test_update.py index 9c8e95fcb78..46235fd778f 100644 --- a/ckan/new_tests/logic/action/test_update.py +++ b/ckan/new_tests/logic/action/test_update.py @@ -130,11 +130,23 @@ def test_user_update_with_short_password(self): helpers.call_action('user_update', **user) def test_user_update_with_empty_password(self): - user = helpers.call_action('user_create', **data.typical_user()) + '''If an empty password is passed to user_update, nothing should + happen. - user['password'] = '' - with nose.tools.assert_raises(logic.ValidationError) as context: - helpers.call_action('user_update', **user) + No error (e.g. a validation error) is raised, but the password is not + changed either. + + ''' + user_dict = data.typical_user() + original_password = user_dict['password'] + user_dict = helpers.call_action('user_create', **user_dict) + + user_dict['password'] = '' + helpers.call_action('user_update', **user_dict) + + import ckan.model as model + updated_user = model.User.get(user_dict['id']) + assert updated_user.validate_password(original_password) def test_user_update_with_null_password(self): user = helpers.call_action('user_create', **data.typical_user()) From 51cd5d18c4005f469c6052a6bddc3275b6373cde Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Fri, 26 Jul 2013 19:29:02 +0200 Subject: [PATCH 033/189] [#1117] Tweak a docstring --- ckan/new_tests/data.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ckan/new_tests/data.py b/ckan/new_tests/data.py index c8b534a2a3c..c67058e2c93 100644 --- a/ckan/new_tests/data.py +++ b/ckan/new_tests/data.py @@ -8,6 +8,8 @@ def typical_user(): + '''Return a dictionary representation of a typical, valid CKAN user.''' + return {'name': 'fred', 'email': 'fred@fred.com', 'password': 'wilma', From c6b953496177ca795ac9347eaf7986edb3049ed7 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Fri, 26 Jul 2013 19:32:07 +0200 Subject: [PATCH 034/189] [#1117] Refactor and add docstring to user_name_validator - Add docstring - Use model.User.get() to find whether a user exists, instead of a lot of SQLAlchemy in ckan.logic. This should make it easier to unit test user_name_validator() in isolation, because the tests will only have to mock one method ckan.model.User.get() instead of having to mock several things. Also SQLAlchemy should just be in the model anyway, not in the logic. - Refactor and add code comments to clarify the obscure thing that user_name_validator() does with context['user_obj'] on user_update()s. This was completely obscure before, now hopefully it's clearer. (But it's a bad design anyway, user_create and user_update shouldn't be sharing the same user_name_validator function.) I don't *think* I broke anything by refactoring this (tests are still passing). --- ckan/logic/validators.py | 42 ++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/ckan/logic/validators.py b/ckan/logic/validators.py index a18cf783d64..a32d3ce3d28 100644 --- a/ckan/logic/validators.py +++ b/ckan/logic/validators.py @@ -493,23 +493,37 @@ def ignore_not_group_admin(key, data, errors, context): data.pop(key) def user_name_validator(key, data, errors, context): - model = context["model"] - session = context["session"] - user = context.get("user_obj") + '''Validate a new user name. + + Append an error message to ``errors[key]`` if a user named ``data[key]`` + already exists. Otherwise, do nothing. + + :raises ckan.lib.navl.dictization_functions.Invalid: if ``data[key]`` is + not a string + :rtype: None + + ''' + model = context['model'] + new_user_name = data[key] - if not isinstance(data[key], basestring): + if not isinstance(new_user_name, basestring): raise Invalid(_('User names must be strings')) - query = session.query(model.User.name).filter_by(name=data[key]) - if user: - user_id = user.id - else: - user_id = data.get(key[:-1] + ("id",)) - if user_id and user_id is not missing: - query = query.filter(model.User.id <> user_id) - result = query.first() - if result: - errors[key].append(_('That login name is not available.')) + user = model.User.get(new_user_name) + if user is not None: + # A user with new_user_name already exists in the database. + + user_obj_from_context = context.get('user_obj') + if user_obj_from_context and user_obj_from_context.id == user.id: + # If there's a user_obj in context with the same id as the user + # found in the db, then we must be doing a user_update and not + # updating the user name, so don't return an error. + return + else: + # Otherwise return an error: there's already another user with that + # name, so you can create a new user with that name or update an + # existing user's name to that name. + errors[key].append(_('That login name is not available.')) def user_both_passwords_entered(key, data, errors, context): From 93d0d819cc68cc3e84b472eefdf4fa3f26f32c07 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Fri, 26 Jul 2013 20:21:35 +0200 Subject: [PATCH 035/189] [#1117] Mock ckan.model in user_name_validator() tests Now that user_name_validator() has been refactored to call model.User.get() instead of doing its own SQLAlchemy (commit c6b953), it's really easy to mock ckan.model in the user_name_validator() unit tests by just mocking the single method ckan.model.User.get(). This means the user_name_validator() unit tests no longer touch the disk or db or bring in ckan.model. --- ckan/new_tests/logic/test_validators.py | 56 +++++++++++++------------ 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/ckan/new_tests/logic/test_validators.py b/ckan/new_tests/logic/test_validators.py index d6556532347..712f3787672 100644 --- a/ckan/new_tests/logic/test_validators.py +++ b/ckan/new_tests/logic/test_validators.py @@ -4,6 +4,7 @@ ''' import copy +import mock import nose.tools import ckan.new_tests.helpers as helpers @@ -109,7 +110,6 @@ def test_user_name_validator_with_non_string_value(self): ''' import ckan.logic.validators as validators - import ckan.model as model import ckan.lib.navl.dictization_functions as df non_string_values = [ @@ -126,13 +126,12 @@ def test_user_name_validator_with_non_string_value(self): lambda x: x**2, ] - key = ('name',) - context = { - 'model': model, - 'session': model.Session, - 'user': '', - } + # Mock ckan.model. + mock_model = mock.MagicMock() + # model.User.get(some_user_id) needs to return None for this test. + mock_model.User.get.return_value = None + key = ('name',) for non_string_value in non_string_values: data = test_data.validator_data_dict() data[key] = non_string_value @@ -144,7 +143,8 @@ def test_user_name_validator_with_non_string_value(self): original_errors = copy.deepcopy(errors) with nose.tools.assert_raises(df.Invalid): - validators.user_name_validator(key, data, errors, context) + validators.user_name_validator(key, data, errors, + context={'model': mock_model}) assert data == original_data, ("user_name_validator shouldn't " 'modify the data dict') @@ -158,28 +158,25 @@ def test_user_name_validator_with_a_name_that_already_exists(self): ''' import ckan.logic.validators as validators - import ckan.model as model - existing_user = helpers.call_action('user_create', - **test_data.typical_user()) + # Mock ckan.model. model.User.get('user_name') will return another mock + # object rather than None, which will simulate an existing user with + # the same user name in the database. + mock_model = mock.MagicMock() - # Try to create another user with the same name as the existing user. data = test_data.validator_data_dict() key = ('name',) - data[key] = existing_user['name'] + data[key] = 'user_name' errors = test_data.validator_errors_dict() errors[key] = [] - context = { - 'model': model, - 'session': model.Session, - 'user': '', - } # Make copies of the data and errors dicts for asserting later. original_data = copy.deepcopy(data) original_errors = copy.deepcopy(errors) - result = validators.user_name_validator(key, data, errors, context) + # Try to create another user with the same name as the existing user. + result = validators.user_name_validator(key, data, errors, + context={'model': mock_model}) assert result is None, ("user_name_validator() shouldn't return " "anything") @@ -201,24 +198,25 @@ def test_user_name_validator_successful(self): '''user_name_validator() should do nothing if given a valid name.''' import ckan.logic.validators as validators - import ckan.model as model data = test_data.validator_data_dict() key = ('name',) data[key] = 'new_user_name' errors = test_data.validator_errors_dict() errors[key] = [] - context = { - 'model': model, - 'session': model.Session, - 'user': '', - } + + # Mock ckan.model. + mock_model = mock.MagicMock() + # model.User.get(user_name) should return None, to simulate that no + # user with that name exists in the database. + mock_model.User.get.return_value = None # Make copies of the data and errors dicts for asserting later. original_data = copy.deepcopy(data) original_errors = copy.deepcopy(errors) - result = validators.user_name_validator(key, data, errors, context) + result = validators.user_name_validator(key, data, errors, + context={'model': mock_model}) assert result is None, ("user_name_validator() shouldn't return " 'anything') @@ -227,4 +225,8 @@ def test_user_name_validator_successful(self): 'the data dict') assert errors == original_errors, ("user_name_validator shouldn't " - 'modify the errors dict') + 'modify the errors dict if given a' + 'valid user name') + + # TODO: Test user_name_validator()'s behavior when there's a 'user_obj' in + # the context dict. From eb6fd0a5ab53f60b0a43a797abcd62a8a45d84f0 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Tue, 30 Jul 2013 15:54:21 +0200 Subject: [PATCH 036/189] [#1117] Add factory_boy to action tests Add a new module ckan.new_tests.factories with (so far) just a User factory class that uses the factory_boy library to create CKAN users. Update the ckan.new_tests.logic.action.test_update tests to use this factory instead of ckan.new_tests.data. Still need to update the rest of the new tests to use the factory (and also add factories for other classes such as dataset etc.), then data.py can be deleted. Still need to add factory_boy to dev-requirements.txt. --- ckan/new_tests/factories.py | 80 ++++++++++++++++++++++ ckan/new_tests/logic/action/test_update.py | 38 +++++----- 2 files changed, 99 insertions(+), 19 deletions(-) create mode 100644 ckan/new_tests/factories.py diff --git a/ckan/new_tests/factories.py b/ckan/new_tests/factories.py new file mode 100644 index 00000000000..e338c1caa1b --- /dev/null +++ b/ckan/new_tests/factories.py @@ -0,0 +1,80 @@ +'''A collection of factory classes for building CKAN users, datasets, etc. + +These are meant to be used by tests to create any objects or "test fixtures" +that are needed for the tests. They're written using factory_boy: + + http://factoryboy.readthedocs.org/en/latest/ + +These are not meant to be used for the actual testing, e.g. if you're writing a +test for the user_create action function then call user_create, don't test it +via the User factory below. + +Usage: + + # Create a user with the factory's default attributes, and get back a + # user dict: + user_dict = factories.User() + + # You can create a second user the same way. For attributes that can't be + # the same (e.g. you can't have two users with the same name) a new value + # will be generated each time you use the factory: + another_user_dict = factories.User() + + # Create a user and specify your own user name and email (this works + # with any params that CKAN's user_create() accepts): + custom_user_dict = factories.User(name='bob', email='bob@bob.com') + + # Get a user dict containing the attributes (name, email, password, etc.) + # that the factory would use to create a user, but without actually + # creating the user in CKAN: + user_attributes_dict = factories.User.attributes() + + # If you later want to create a user using these attributes, just pass them + # to the factory: + user = factories.User(**user_attributes_dict) + +''' +import factory + +import ckan.model +import ckan.logic +import ckan.new_tests.helpers as helpers + + +class User(factory.Factory): + '''A factory class for creating CKAN users.''' + + # This is the class that UserFactory will create and return instances + # of. + FACTORY_FOR = ckan.model.User + + # These are the default params that will be used to create new users. + fullname = 'Mr. Test User' + password = 'pass' + about = 'Just another test user.' + + # Generate a different user name param for each user that gets created. + name = factory.Sequence( + lambda n: 'test_user_{n}'.format(n=n)) + + # Compute the email param for each user based on the values of the other + # params above. + email = factory.LazyAttribute( + lambda a: '{0}@ckan.org'.format(a.name).lower()) + + # I'm not sure how to support factory_boy's .build() feature in CKAN, + # so I've disabled it here. + @classmethod + def _build(cls, target_class, *args, **kwargs): + raise NotImplementedError(".build() isn't supported in CKAN") + + # To make factory_boy work with CKAN we override _create() and make it call + # a CKAN action function. + # We might also be able to do this by using factory_boy's direct SQLAlchemy + # support: http://factoryboy.readthedocs.org/en/latest/orms.html#sqlalchemy + @classmethod + def _create(cls, target_class, *args, **kwargs): + if args: + assert False, "Positional args aren't supported, use keyword args." + user_dict = helpers.call_action('user_create', **kwargs) + return user_dict diff --git a/ckan/new_tests/logic/action/test_update.py b/ckan/new_tests/logic/action/test_update.py index 46235fd778f..d94a930ae0f 100644 --- a/ckan/new_tests/logic/action/test_update.py +++ b/ckan/new_tests/logic/action/test_update.py @@ -6,7 +6,7 @@ import ckan.logic as logic import ckan.new_tests.helpers as helpers -import ckan.new_tests.data as data +import ckan.new_tests.factories as factories def datetime_from_string(s): @@ -49,7 +49,7 @@ def test_user_update_name(self): # 4. Do absolutely nothing else! # 1. Setup. - user = helpers.call_action('user_create', **data.typical_user()) + user = factories.User() # 2. Call the function that is being tested, once only. # FIXME we have to pass the email address and password to user_update @@ -57,7 +57,7 @@ def test_user_update_name(self): # fails. helpers.call_action('user_update', id=user['name'], email=user['email'], - password=data.typical_user()['password'], + password=factories.User.attributes()['password'], name='updated', ) @@ -70,21 +70,22 @@ def test_user_update_name(self): # 4. Do absolutely nothing else! def test_user_update_with_id_that_does_not_exist(self): - user_dict = data.typical_user() + user_dict = factories.User.attributes() user_dict['id'] = "there's no user with this id" + with nose.tools.assert_raises(logic.NotFound) as context: helpers.call_action('user_update', **user_dict) # TODO: Could assert the actual error message, not just the exception? # (Could also do this with many of the tests below.) def test_user_update_with_no_id(self): - user_dict = data.typical_user() + user_dict = factories.User.attributes() assert 'id' not in user_dict with nose.tools.assert_raises(logic.ValidationError) as context: helpers.call_action('user_update', **user_dict) def test_user_update_with_invalid_name(self): - user = helpers.call_action('user_create', **data.typical_user()) + user = factories.User() invalid_names = ('', 'a', False, 0, -1, 23, 'new', 'edit', 'search', 'a'*200, 'Hi!', ) @@ -94,9 +95,8 @@ def test_user_update_with_invalid_name(self): helpers.call_action('user_update', **user) def test_user_update_to_name_that_already_exists(self): - fred = helpers.call_action('user_create', **data.typical_user()) - bob = helpers.call_action('user_create', name='bob', - email='bob@bob.com', password='pass') + fred = factories.User(name='fred') + bob = factories.User(name='bob') # Try to update fred and change his user name to bob, which is already # bob's user name @@ -107,7 +107,7 @@ def test_user_update_to_name_that_already_exists(self): def test_user_update_password(self): '''Test that updating a user's password works successfully.''' - user = helpers.call_action('user_create', **data.typical_user()) + user = factories.User() # FIXME we have to pass the email address to user_update even though # we're not updating it, otherwise validation fails. @@ -123,7 +123,7 @@ def test_user_update_password(self): assert updated_user.validate_password('new password') def test_user_update_with_short_password(self): - user = helpers.call_action('user_create', **data.typical_user()) + user = factories.User() user['password'] = 'xxx' # This password is too short. with nose.tools.assert_raises(logic.ValidationError) as context: @@ -137,9 +137,9 @@ def test_user_update_with_empty_password(self): changed either. ''' - user_dict = data.typical_user() + user_dict = factories.User.attributes() original_password = user_dict['password'] - user_dict = helpers.call_action('user_create', **user_dict) + user_dict = factories.User(**user_dict) user_dict['password'] = '' helpers.call_action('user_update', **user_dict) @@ -149,14 +149,14 @@ def test_user_update_with_empty_password(self): assert updated_user.validate_password(original_password) def test_user_update_with_null_password(self): - user = helpers.call_action('user_create', **data.typical_user()) + user = factories.User() user['password'] = None with nose.tools.assert_raises(logic.ValidationError) as context: helpers.call_action('user_update', **user) def test_user_update_with_invalid_password(self): - user = helpers.call_action('user_create', **data.typical_user()) + user = factories.User() for password in (False, -1, 23, 30.7): user['password'] = password @@ -168,7 +168,7 @@ def test_user_update_with_invalid_password(self): def test_user_update_activity_stream(self): '''Test that the right activity is emitted when updating a user.''' - user = helpers.call_action('user_create', **data.typical_user()) + user = factories.User() before = datetime.datetime.now() # FIXME we have to pass the email address and password to user_update @@ -176,7 +176,7 @@ def test_user_update_activity_stream(self): # fails. helpers.call_action('user_update', id=user['name'], email=user['email'], - password=data.typical_user()['password'], + password=factories.User.attributes()['password'], name='updated', ) @@ -203,7 +203,7 @@ def test_user_update_with_custom_schema(self): ''' import ckan.logic.schema - user = helpers.call_action('user_create', **data.typical_user()) + user = factories.User() # A mock validator method, it doesn't do anything but it records what # params it gets called with and how many times. @@ -219,7 +219,7 @@ def test_user_update_with_custom_schema(self): # trying to update them, or validation fails. helpers.call_action('user_update', context={'schema': schema}, id=user['name'], email=user['email'], - password=data.typical_user()['password'], + password=factories.User.attributes()['password'], name='updated', ) From c556fabfd5c43b3ff0e775462b0831f1d59cee99 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Tue, 30 Jul 2013 16:20:25 +0200 Subject: [PATCH 037/189] [#1117] Use factory in auth/test_update.py Instead of data.py. --- ckan/new_tests/logic/auth/test_update.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/ckan/new_tests/logic/auth/test_update.py b/ckan/new_tests/logic/auth/test_update.py index 49bf163178f..be58fe89407 100644 --- a/ckan/new_tests/logic/auth/test_update.py +++ b/ckan/new_tests/logic/auth/test_update.py @@ -2,7 +2,7 @@ ''' import ckan.new_tests.helpers as helpers -import ckan.new_tests.data as data +import ckan.new_tests.factories as factories class TestUpdate(object): @@ -35,7 +35,7 @@ def _call_auth(self, auth_name, context=None, **kwargs): def test_user_update_visitor_cannot_update_user(self): '''Visitors should not be able to update users' accounts.''' - user = helpers.call_action('user_create', **data.typical_user()) + user = factories.User() user['name'] = 'updated' # Try to update the user, but without passing any API key. @@ -49,9 +49,8 @@ def test_user_update_visitor_cannot_update_user(self): def test_user_update_user_cannot_update_another_user(self): '''Users should not be able to update other users' accounts.''' - fred = helpers.call_action('user_create', **data.typical_user()) - bob = helpers.call_action('user_create', name='bob', - email='bob@bob.com', password='pass') + fred = factories.User(name='fred') + bob = factories.User(name='bob') fred['name'] = 'updated' # Make Bob try to update Fred's user account. @@ -62,7 +61,7 @@ def test_user_update_user_cannot_update_another_user(self): def test_user_update_user_can_update_herself(self): '''Users should be authorized to update their own accounts.''' - user = helpers.call_action('user_create', **data.typical_user()) + user = factories.User() context = {'user': user['name']} user['name'] = 'updated' From a5fe9e6e231dbc64f729427323c677d415c60744 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Tue, 30 Jul 2013 16:25:15 +0200 Subject: [PATCH 038/189] [#1117] Remove data.py from test_validators.py Move the helper functions it was using into factories.py and make it use that instead. --- ckan/new_tests/factories.py | 16 ++++++++++++++++ ckan/new_tests/logic/test_validators.py | 14 +++++++------- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/ckan/new_tests/factories.py b/ckan/new_tests/factories.py index e338c1caa1b..b68fa7789f7 100644 --- a/ckan/new_tests/factories.py +++ b/ckan/new_tests/factories.py @@ -78,3 +78,19 @@ def _create(cls, target_class, *args, **kwargs): assert False, "Positional args aren't supported, use keyword args." user_dict = helpers.call_action('user_create', **kwargs) return user_dict + + +def validator_data_dict(): + '''Return a data dict with some arbitrary data in it, suitable to be passed + to validator functions for testing. + + ''' + return {('other key',): 'other value'} + + +def validator_errors_dict(): + '''Return an errors dict with some arbitrary errors in it, suitable to be + passed to validator functions for testing. + + ''' + return {('other key',): ['other error']} diff --git a/ckan/new_tests/logic/test_validators.py b/ckan/new_tests/logic/test_validators.py index 712f3787672..20347575db0 100644 --- a/ckan/new_tests/logic/test_validators.py +++ b/ckan/new_tests/logic/test_validators.py @@ -8,7 +8,7 @@ import nose.tools import ckan.new_tests.helpers as helpers -import ckan.new_tests.data as test_data +import ckan.new_tests.factories as factories class TestValidators(object): @@ -133,9 +133,9 @@ def test_user_name_validator_with_non_string_value(self): key = ('name',) for non_string_value in non_string_values: - data = test_data.validator_data_dict() + data = factories.validator_data_dict() data[key] = non_string_value - errors = test_data.validator_errors_dict() + errors = factories.validator_errors_dict() errors[key] = [] # Make copies of the data and errors dicts for asserting later. @@ -164,10 +164,10 @@ def test_user_name_validator_with_a_name_that_already_exists(self): # the same user name in the database. mock_model = mock.MagicMock() - data = test_data.validator_data_dict() + data = factories.validator_data_dict() key = ('name',) data[key] = 'user_name' - errors = test_data.validator_errors_dict() + errors = factories.validator_errors_dict() errors[key] = [] # Make copies of the data and errors dicts for asserting later. @@ -199,10 +199,10 @@ def test_user_name_validator_successful(self): import ckan.logic.validators as validators - data = test_data.validator_data_dict() + data = factories.validator_data_dict() key = ('name',) data[key] = 'new_user_name' - errors = test_data.validator_errors_dict() + errors = factories.validator_errors_dict() errors[key] = [] # Mock ckan.model. From fe8eec0f4bde1f4b23d71d49584e434c78148d91 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Tue, 30 Jul 2013 16:29:09 +0200 Subject: [PATCH 039/189] [#1117] Use factories instead of data in navl/test_validators.py So data.py is now unused and we can remove it. --- ckan/new_tests/data.py | 32 ---------------------- ckan/new_tests/lib/navl/test_validators.py | 10 +++---- 2 files changed, 5 insertions(+), 37 deletions(-) delete mode 100644 ckan/new_tests/data.py diff --git a/ckan/new_tests/data.py b/ckan/new_tests/data.py deleted file mode 100644 index c67058e2c93..00000000000 --- a/ckan/new_tests/data.py +++ /dev/null @@ -1,32 +0,0 @@ -'''A collection of static data for use in tests. - -These are functions that return objects because the data is intended to be -read-only: if they were simply module-level objects then one test may modify -eg. a dict or list and then break the tests that follow it. - -''' - - -def typical_user(): - '''Return a dictionary representation of a typical, valid CKAN user.''' - - return {'name': 'fred', - 'email': 'fred@fred.com', - 'password': 'wilma', - } - - -def validator_data_dict(): - '''Return a data dict with some arbitrary data in it, suitable to be passed - to validator functions for testing. - - ''' - return {('other key',): 'other value'} - - -def validator_errors_dict(): - '''Return an errors dict with some arbitrary errors in it, suitable to be - passed to validator functions for testing. - - ''' - return {('other key',): ['other error']} diff --git a/ckan/new_tests/lib/navl/test_validators.py b/ckan/new_tests/lib/navl/test_validators.py index 51df87da648..cbebfddd467 100644 --- a/ckan/new_tests/lib/navl/test_validators.py +++ b/ckan/new_tests/lib/navl/test_validators.py @@ -6,7 +6,7 @@ import nose.tools -import ckan.new_tests.data as test_data +import ckan.new_tests.factories as factories class TestValidators(object): @@ -28,12 +28,12 @@ def test_ignore_missing_with_value_missing(self): key = ('key to be validated',) # The data to pass to the validator function for validation. - data = test_data.validator_data_dict() + data = factories.validator_data_dict() if value != 'skip': data[key] = value # The errors dict to pass to the validator function. - errors = test_data.validator_errors_dict() + errors = factories.validator_errors_dict() errors[key] = [] # Make copies of the data and errors dicts for asserting later. @@ -66,9 +66,9 @@ def test_ignore_missing_with_a_value(self): import ckan.lib.navl.validators as validators key = ('key to be validated',) - data = test_data.validator_data_dict() + data = factories.validator_data_dict() data[key] = 'value to be validated' - errors = test_data.validator_errors_dict() + errors = factories.validator_errors_dict() errors[key] = [] # Make copies of the data and errors dicts for asserting later. From 3e7f6900882804a1a0b9560333654306f4e8cee1 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 30 Jul 2013 15:30:27 +0100 Subject: [PATCH 040/189] [#1126] First run at new homepage modules core extension --- ckan/public/base/less/homepage.less | 112 +++++++----------- ckan/templates/home/index.html | 93 +-------------- ckanext/homepage/__init__.py | 0 ckanext/homepage/plugin.py | 24 ++++ .../homepage/theme/templates/home/index.html | 36 ++++++ .../home/snippets/featured_group.html | 3 + .../home/snippets/featured_organization.html | 3 + .../templates/home/snippets/promoted.html | 20 ++++ .../theme/templates/home/snippets/search.html | 18 +++ setup.py | 1 + 10 files changed, 150 insertions(+), 160 deletions(-) create mode 100644 ckanext/homepage/__init__.py create mode 100644 ckanext/homepage/plugin.py create mode 100644 ckanext/homepage/theme/templates/home/index.html create mode 100644 ckanext/homepage/theme/templates/home/snippets/featured_group.html create mode 100644 ckanext/homepage/theme/templates/home/snippets/featured_organization.html create mode 100644 ckanext/homepage/theme/templates/home/snippets/promoted.html create mode 100644 ckanext/homepage/theme/templates/home/snippets/search.html diff --git a/ckan/public/base/less/homepage.less b/ckan/public/base/less/homepage.less index 0aaa8a839ab..02574845817 100644 --- a/ckan/public/base/less/homepage.less +++ b/ckan/public/base/less/homepage.less @@ -1,26 +1,24 @@ -.hero { - background: url("@{imagePath}/background-tile.png"); - padding: 20px 0; - min-height: 0; - > .container { - position: relative; - padding-bottom: 0; - } - .search-giant { - margin-bottom: 10px; - input { - border-color: darken(@mastheadBackgroundColorEnd, 5); - } +.homepage { + + [role=main] { + padding: 20px 0; } - .page-heading { - font-size: 18px; - margin-bottom: 0; + + .row { + position: relative; } - .module-dark { + + .module-search { padding: 5px; margin-bottom: 0; color: @mastheadTextColor; background: @layoutTrimBackgroundColor; + .search-giant { + margin-bottom: 10px; + input { + border-color: darken(@mastheadBackgroundColorEnd, 5); + } + } .module-content { .border-radius(3px 3px 0 0); background-color: @mastheadBackgroundColor; @@ -32,66 +30,38 @@ line-height: 40px; } } - } - .tags { - .clearfix(); - padding: 5px 10px 10px 10px; - background-color: darken(@mastheadBackgroundColor, 10%); - .border-radius(0 0 3px 3px); - h3, - .tag { - display: block; - float: left; - margin: 5px 10px 0 0; - } - h3 { - font-size: @baseFontSize; - line-height: @baseLineHeight; - padding: 2px 8px; + .tags { + .clearfix(); + padding: 5px 10px 10px 10px; + background-color: darken(@mastheadBackgroundColor, 10%); + .border-radius(0 0 3px 3px); + h3, + .tag { + display: block; + float: left; + margin: 5px 10px 0 0; + } + h3 { + font-size: @baseFontSize; + line-height: @baseLineHeight; + padding: 2px 8px; + } } } -} -.hero-primary, -.hero-secondary { - .makeColumn(6); -} - -.hero-primary { - margin-left: 0; // Remove grid margin. - margin-bottom: 0; -} + .group-list { + margin: 0; + } -.hero-secondary { - position: absolute; - bottom: 0; - right: 0; - .hero-secondary-inner { + .slot2 { + position: absolute; bottom: 0; - left: 0; right: 0; - } -} - -.main.homepage { - padding-top: 20px; - padding-bottom: 20px; - border-top: 1px solid @layoutTrimBorderColor; - .module-heading .media-image { - margin-right: 15px; - max-height: 53px; - .box-shadow(0 0 0 2px rgba(255, 255, 255, 0.5)); - } - .group-listing .box { - min-height: 275px; - } - .group-list { - margin-bottom: 0; - .dataset-content { - min-height: 70px; + .module-search { + bottom: 0; + left: 0; + right: 0; } } - .box .module { - margin-top: 0; - } + } diff --git a/ckan/templates/home/index.html b/ckan/templates/home/index.html index 2508f8b8508..ee1d1f4c8ae 100644 --- a/ckan/templates/home/index.html +++ b/ckan/templates/home/index.html @@ -3,98 +3,13 @@ {% block subtitle %}{{ _("Welcome") }}{% endblock %} {% block maintag %}{% endblock %} +{% block toolbar %}{% endblock %} {% block content %} -
-
- {{ self.flash() }} - {{ self.primary_content() }} -
-
-
+
- {{ self.secondary_content() }} -
-
-{% endblock %} - -{% block primary_content %} -
-
- {% block home_primary %} -
- {% if g.site_intro_text %} - {{ h.render_markdown(g.site_intro_text) }} - {% else %} - {% block home_primary_content %} -

{% block home_primary_heading %}{{ _("Welcome to CKAN") }}{% endblock %}

-

- {% block home_primary_text %} - {% trans %}This is a nice introductory paragraph about CKAN or the site - in general. We don't have any copy to go here yet but soon we will - {% endtrans %} - {% endblock %} -

- {% endblock %} - {% endif %} -
- {% endblock %} - - {% block home_image %} - - {% endblock %} -
-
-
-
- {% block home_secondary_content %} -
- {% block home_search %} -
-

{{ _("Search Your Data") }}

-
- - -
-
- {% endblock %} - {% block home_tags %} -
-

{{ _('Popular Tags') }}

- {% set tags = h.get_facet_items_dict('tags', limit=3) %} - {% for tag in tags %} - {{ h.truncate(tag.display_name, 22) }} - {% endfor %} -
- {% endblock %} -
- {% endblock %} + {{ self.flash() }}
+ {% block primary_content %}{% endblock %}
{% endblock %} - -{% block secondary_content %} -
- {% for group in c.group_package_stuff %} -
-
- {% snippet 'snippets/group_item.html', group=group.group_dict, truncate=50, truncate_title=35 %} -
-
- {% endfor %} -
-{% endblock %} - -{# Remove the toolbar. #} - -{% block toolbar %}{% endblock %} diff --git a/ckanext/homepage/__init__.py b/ckanext/homepage/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ckanext/homepage/plugin.py b/ckanext/homepage/plugin.py new file mode 100644 index 00000000000..ef429f0457d --- /dev/null +++ b/ckanext/homepage/plugin.py @@ -0,0 +1,24 @@ +import logging + +import ckan.plugins as p + +log = logging.getLogger(__name__) + +class HomepagePlugin(p.SingletonPlugin): + p.implements(p.IConfigurer, inherit=True) + p.implements(p.IConfigurable, inherit=True) + + def update_config(self, config): + p.toolkit.add_template_directory(config, 'theme/templates') + + def get_featured_organization(self): + return + + def get_featured_group(self): + return + + def get_helpers(self): + return { + 'get_featured_organization': self.get_featured_organization, + 'get_featured_group': self.get_featured_group, + } diff --git a/ckanext/homepage/theme/templates/home/index.html b/ckanext/homepage/theme/templates/home/index.html new file mode 100644 index 00000000000..0a9d0de727b --- /dev/null +++ b/ckanext/homepage/theme/templates/home/index.html @@ -0,0 +1,36 @@ +{% ckan_extends %} + +{% block primary_content %} +
+
+
+
{{ self.homepage_slot_1() }}
+
{{ self.homepage_slot_2() }}
+
+
+
+
+
+
+
{{ self.homepage_slot_3() }}
+
{{ self.homepage_slot_4() }}
+
+
+
+{% endblock %} + +{% block homepage_slot_1 %} + {% snippet 'home/snippets/promoted.html', intro=g.site_intro_text %} +{% endblock %} + +{% block homepage_slot_2 %} + {% snippet 'home/snippets/search.html', query=c.q, tags=h.get_facet_items_dict('tags', limit=3), placeholder=_('eg. Gold Prices') %} +{% endblock %} + +{% block homepage_slot_3 %} + {% snippet 'home/snippets/featured_group.html', group=h.get_featured_group() %} +{% endblock %} + +{% block homepage_slot_4 %} + {% snippet 'home/snippets/featured_organization.html', organization=h.get_featured_organization() %} +{% endblock %} diff --git a/ckanext/homepage/theme/templates/home/snippets/featured_group.html b/ckanext/homepage/theme/templates/home/snippets/featured_group.html new file mode 100644 index 00000000000..bdd20fafdbe --- /dev/null +++ b/ckanext/homepage/theme/templates/home/snippets/featured_group.html @@ -0,0 +1,3 @@ +
+ {% snippet 'snippets/group_item.html', group=group, truncate=50, truncate_title=35 %} +
diff --git a/ckanext/homepage/theme/templates/home/snippets/featured_organization.html b/ckanext/homepage/theme/templates/home/snippets/featured_organization.html new file mode 100644 index 00000000000..760f8b5e479 --- /dev/null +++ b/ckanext/homepage/theme/templates/home/snippets/featured_organization.html @@ -0,0 +1,3 @@ +
+ {% snippet 'snippets/organization_item.html', organization=organization, truncate=50, truncate_title=35 %} +
diff --git a/ckanext/homepage/theme/templates/home/snippets/promoted.html b/ckanext/homepage/theme/templates/home/snippets/promoted.html new file mode 100644 index 00000000000..287083a51b0 --- /dev/null +++ b/ckanext/homepage/theme/templates/home/snippets/promoted.html @@ -0,0 +1,20 @@ +
+
+ {% if intro %} + {{ h.render_markdown(intro) }} + {% else %} +

{{ _("Welcome to CKAN") }}

+

+ {% trans %}This is a nice introductory paragraph about CKAN or the site + in general. We don't have any copy to go here yet but soon we will + {% endtrans %} +

+ {% endif %} +
+ +
diff --git a/ckanext/homepage/theme/templates/home/snippets/search.html b/ckanext/homepage/theme/templates/home/snippets/search.html new file mode 100644 index 00000000000..586ae5dba6a --- /dev/null +++ b/ckanext/homepage/theme/templates/home/snippets/search.html @@ -0,0 +1,18 @@ + diff --git a/setup.py b/setup.py index f4e97942047..958db3cf788 100644 --- a/setup.py +++ b/setup.py @@ -173,6 +173,7 @@ [ckan.system_plugins] domain_object_mods = ckan.model.modification:DomainObjectModificationExtension + homepage = ckanext.homepage.plugin:HomepagePlugin [ckan.test_plugins] routes_plugin=tests.ckantestplugins:RoutesPlugin From 7910f78ce23a049eb5cdd53efa1f2f117f65b0b9 Mon Sep 17 00:00:00 2001 From: John Martin Date: Wed, 31 Jul 2013 10:31:39 +0100 Subject: [PATCH 041/189] [#1126] Added config for sysadmin area --- ckan/templates/admin/config.html | 6 +++++- .../theme/templates/admin/config.html | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 ckanext/homepage/theme/templates/admin/config.html diff --git a/ckan/templates/admin/config.html b/ckan/templates/admin/config.html index 0ec27d1e687..64fd8558b28 100644 --- a/ckan/templates/admin/config.html +++ b/ckan/templates/admin/config.html @@ -4,7 +4,9 @@ {% block primary_content_inner %}
- {{ autoform.generate(form_items, data, errors) }} + {% block admin_form %} + {{ autoform.generate(form_items, data, errors) }} + {% endblock %}
{% set locale = h.dump_json({'content': _('Are you sure you want to reset the config?')}) %} {{ _('Reset') }} @@ -20,6 +22,7 @@

{{ _('CKAN config options') }}

+ {% block admin_form_help %} {% set about_url = h.url_for(controller='home', action='about') %} {% set home_url = h.url_for(controller='home', action='index') %} {% set docs_url = "http://docs.ckan.org/en/{0}/theming.html".format(g.ckan_doc_version) %} @@ -39,6 +42,7 @@

the templates more fully we recommend reading the documentation.

{% endtrans %} + {% endblock %}

{% endblock %} diff --git a/ckanext/homepage/theme/templates/admin/config.html b/ckanext/homepage/theme/templates/admin/config.html new file mode 100644 index 00000000000..86e85b47714 --- /dev/null +++ b/ckanext/homepage/theme/templates/admin/config.html @@ -0,0 +1,19 @@ +{% ckan_extends %} + +{% import 'macros/form.html' as form %} + +{% block admin_form %} + {{ super() }} + {{ form.select('homepage_layout', label=_('Homepage'), options=[ + {'value': 1, 'text': 'Introductory area, search, featured group and featured organization'}, + {'value': 2, 'text': 'Search, featured dataset, stats and featured organization'} + ], selected=1, error=false) }} +{% endblock %} + +{% block admin_form_help %} + {{ super() }} + {% trans %} +

Homepage: This is for choosing a predefined layout for + the modules that appear on your homepage.

+ {% endtrans %} +{% endblock %} From a957533216a07e6e03daabb4a78cdd0a17f6695f Mon Sep 17 00:00:00 2001 From: John Martin Date: Wed, 31 Jul 2013 10:42:28 +0100 Subject: [PATCH 042/189] [#1126] Simplified snippets and added temporary homepage layour variable --- ckan/public/base/less/homepage.less | 6 ++- ckan/templates/home/index.html | 2 +- .../homepage/theme/templates/home/index.html | 44 +++++++++++++------ .../home/snippets/featured_group.html | 2 + .../home/snippets/featured_organization.html | 2 + .../templates/home/snippets/promoted.html | 2 + .../theme/templates/home/snippets/search.html | 5 ++- 7 files changed, 45 insertions(+), 18 deletions(-) diff --git a/ckan/public/base/less/homepage.less b/ckan/public/base/less/homepage.less index 02574845817..fc185cb3098 100644 --- a/ckan/public/base/less/homepage.less +++ b/ckan/public/base/less/homepage.less @@ -53,7 +53,10 @@ margin: 0; } - .slot2 { +} + +.homepage-1 { + .row1 .col2 { position: absolute; bottom: 0; right: 0; @@ -63,5 +66,4 @@ right: 0; } } - } diff --git a/ckan/templates/home/index.html b/ckan/templates/home/index.html index ee1d1f4c8ae..d9e4abfb17b 100644 --- a/ckan/templates/home/index.html +++ b/ckan/templates/home/index.html @@ -6,7 +6,7 @@ {% block toolbar %}{% endblock %} {% block content %} -
+
{{ self.flash() }}
diff --git a/ckanext/homepage/theme/templates/home/index.html b/ckanext/homepage/theme/templates/home/index.html index 0a9d0de727b..bf9addfcb4b 100644 --- a/ckanext/homepage/theme/templates/home/index.html +++ b/ckanext/homepage/theme/templates/home/index.html @@ -1,36 +1,52 @@ {% ckan_extends %} +{% set homepage_layout = 1 %} + +{% block homepage_layout_class %} homepage-{{ homepage_layout }}{% endblock %} + {% block primary_content %}
-
-
{{ self.homepage_slot_1() }}
-
{{ self.homepage_slot_2() }}
+
+
{{ self.row1_col1() }}
+
{{ self.row1_col2() }}
-
-
{{ self.homepage_slot_3() }}
-
{{ self.homepage_slot_4() }}
+
+
{{ self.row2_col1() }}
+
{{ self.row2_col2() }}
{% endblock %} -{% block homepage_slot_1 %} - {% snippet 'home/snippets/promoted.html', intro=g.site_intro_text %} +{% block row1_col1 %} + {% if homepage_layout == 1 %} + {% snippet 'home/snippets/promoted.html' %} + {% elif homepage_layout == 2 %} + {% snippet 'home/snippets/search.html' %} + {% endif %} {% endblock %} -{% block homepage_slot_2 %} - {% snippet 'home/snippets/search.html', query=c.q, tags=h.get_facet_items_dict('tags', limit=3), placeholder=_('eg. Gold Prices') %} +{% block row1_col2 %} + {% if homepage_layout == 1 %} + {% snippet 'home/snippets/search.html' %} + {% elif homepage_layout == 2 %} + {% snippet 'home/snippets/featured_dataset.html' %} + {% endif %} {% endblock %} -{% block homepage_slot_3 %} - {% snippet 'home/snippets/featured_group.html', group=h.get_featured_group() %} +{% block row2_col1 %} + {% if homepage_layout == 1 %} + {% snippet 'home/snippets/featured_group.html' %} + {% elif homepage_layout == 2 %} + {% snippet 'home/snippets/stats.html' %} + {% endif %} {% endblock %} -{% block homepage_slot_4 %} - {% snippet 'home/snippets/featured_organization.html', organization=h.get_featured_organization() %} +{% block row2_col2 %} + {% snippet 'home/snippets/featured_organization.html' %} {% endblock %} diff --git a/ckanext/homepage/theme/templates/home/snippets/featured_group.html b/ckanext/homepage/theme/templates/home/snippets/featured_group.html index bdd20fafdbe..fb5ff22a640 100644 --- a/ckanext/homepage/theme/templates/home/snippets/featured_group.html +++ b/ckanext/homepage/theme/templates/home/snippets/featured_group.html @@ -1,3 +1,5 @@ +{% set group = h.get_featured_group() %} +
{% snippet 'snippets/group_item.html', group=group, truncate=50, truncate_title=35 %}
diff --git a/ckanext/homepage/theme/templates/home/snippets/featured_organization.html b/ckanext/homepage/theme/templates/home/snippets/featured_organization.html index 760f8b5e479..7e43e6c4bcb 100644 --- a/ckanext/homepage/theme/templates/home/snippets/featured_organization.html +++ b/ckanext/homepage/theme/templates/home/snippets/featured_organization.html @@ -1,3 +1,5 @@ +{% set organization = h.get_featured_organization() %} +
{% snippet 'snippets/organization_item.html', organization=organization, truncate=50, truncate_title=35 %}
diff --git a/ckanext/homepage/theme/templates/home/snippets/promoted.html b/ckanext/homepage/theme/templates/home/snippets/promoted.html index 287083a51b0..fc09d9a6300 100644 --- a/ckanext/homepage/theme/templates/home/snippets/promoted.html +++ b/ckanext/homepage/theme/templates/home/snippets/promoted.html @@ -1,3 +1,5 @@ +{% set intro = g.site_intro_text %} +
{% if intro %} diff --git a/ckanext/homepage/theme/templates/home/snippets/search.html b/ckanext/homepage/theme/templates/home/snippets/search.html index 586ae5dba6a..e4a72c7e074 100644 --- a/ckanext/homepage/theme/templates/home/snippets/search.html +++ b/ckanext/homepage/theme/templates/home/snippets/search.html @@ -1,8 +1,11 @@ +{% set tags = h.get_facet_items_dict('tags', limit=3) %} +{% set placeholder = _('eg. Gold Prices') %} + From 325fb746730a7dc0098f99402e56909a2e4c3293 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:16:25 +0100 Subject: [PATCH 078/189] [#858] Whitespace cleanup for ckan/templates/group/admins.html --- ckan/templates/group/admins.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ckan/templates/group/admins.html b/ckan/templates/group/admins.html index a682b3ea587..e032eb7e546 100644 --- a/ckan/templates/group/admins.html +++ b/ckan/templates/group/admins.html @@ -3,8 +3,8 @@ {% block subtitle %}{{ _('Administrators') }} - {{ c.group_dict.title or c.group_dict.name }}{% endblock %} {% block primary_content_inner %} -

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

- {% block admins_list %} +

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

+ {% block admins_list %} {% snippet "user/snippets/followers.html", followers=c.admins %} - {% endblock %} + {% endblock %} {% endblock %} From 0a0e3c0d5caf6d598eacc1e7fe7e4a643c6b2778 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:16:53 +0100 Subject: [PATCH 079/189] [#858] Whitespace cleanup ckan/templates/group/base_form_page.html --- ckan/templates/group/base_form_page.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ckan/templates/group/base_form_page.html b/ckan/templates/group/base_form_page.html index 25f817826c7..7d1664aa844 100644 --- a/ckan/templates/group/base_form_page.html +++ b/ckan/templates/group/base_form_page.html @@ -8,8 +8,8 @@ {% endblock %} {% block primary_content_inner %} -

{% block page_heading %}{{ _('Group Form') }}{% endblock %}

- {% block form %} - {{ c.form | safe }} - {% endblock %} +

{% block page_heading %}{{ _('Group Form') }}{% endblock %}

+ {% block form %} + {{ c.form | safe }} + {% endblock %} {% endblock %} From dfc4bb040c53c47cc2bf9a4a647b2d9aafb9145e Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:17:26 +0100 Subject: [PATCH 080/189] [#858] Whitespace cleanup of ckan/templates/group/confirm_delete.html --- ckan/templates/group/confirm_delete.html | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ckan/templates/group/confirm_delete.html b/ckan/templates/group/confirm_delete.html index 06438e7b511..5e810cffa36 100644 --- a/ckan/templates/group/confirm_delete.html +++ b/ckan/templates/group/confirm_delete.html @@ -8,13 +8,13 @@
{% block form %} -

{{ _('Are you sure you want to delete group - {name}?').format(name=c.group_dict.name) }}

-

-

- - -
-

+

{{ _('Are you sure you want to delete group - {name}?').format(name=c.group_dict.name) }}

+

+

+ + +
+

{% endblock %}
From 21f41c688b658ec2509b8281c50cb9bc85ecff54 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:18:01 +0100 Subject: [PATCH 081/189] [#858] Whitespace cleanup ckan/templates/group/confirm_delete_member.html --- ckan/templates/group/confirm_delete_member.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ckan/templates/group/confirm_delete_member.html b/ckan/templates/group/confirm_delete_member.html index aa72d8e193a..dd314ac7af0 100644 --- a/ckan/templates/group/confirm_delete_member.html +++ b/ckan/templates/group/confirm_delete_member.html @@ -8,14 +8,14 @@
{% block form %} -

{{ _('Are you sure you want to delete member - {name}?').format(name=c.user_dict.name) }}

-

-

- - - -
-

+

{{ _('Are you sure you want to delete member - {name}?').format(name=c.user_dict.name) }}

+

+

+ + + +
+

{% endblock %}
From a1d23e6b32058c99c4080a6e6c8e085590511b6b Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:18:41 +0100 Subject: [PATCH 082/189] [#858] Whitespace cleanup ckan/templates/group/followers.html --- ckan/templates/group/followers.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ckan/templates/group/followers.html b/ckan/templates/group/followers.html index 2f3aa14e1e0..37ee035f840 100644 --- a/ckan/templates/group/followers.html +++ b/ckan/templates/group/followers.html @@ -3,8 +3,8 @@ {% block subtitle %}{{ _('Followers') }} - {{ c.group_dict.title or c.group_dict.name }}{% endblock %} {% block primary_content_inner %} -

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

- {% block followers_list %} +

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

+ {% block followers_list %} {% snippet "user/snippets/followers.html", followers=c.followers %} - {% endblock %} + {% endblock %} {% endblock %} From 01758564a95f5698108a34ca792ef95e59fd9313 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:19:21 +0100 Subject: [PATCH 083/189] [#858] Whitespace cleanup ckan/templates/group/history.html --- ckan/templates/group/history.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ckan/templates/group/history.html b/ckan/templates/group/history.html index 5c2089bc0f0..021c85005d8 100644 --- a/ckan/templates/group/history.html +++ b/ckan/templates/group/history.html @@ -3,8 +3,8 @@ {% block subtitle %}{{ _('History') }} - {{ c.group_dict.display_name }}{% endblock %} {% block primary_content_inner %} -

{{ _('History') }}

- {% block group_history_revisions %} - {% snippet "group/snippets/history_revisions.html", group_dict=c.group_dict, group_revisions=c.group_revisions %} - {% endblock %} +

{{ _('History') }}

+ {% block group_history_revisions %} + {% snippet "group/snippets/history_revisions.html", group_dict=c.group_dict, group_revisions=c.group_revisions %} + {% endblock %} {% endblock %} From acf61f61190522b9b5e95fc513fa20ed64737469 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:20:00 +0100 Subject: [PATCH 084/189] [#858] Whitespace cleanup ckan/templates/group/index.html --- ckan/templates/group/index.html | 36 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/ckan/templates/group/index.html b/ckan/templates/group/index.html index 8db2afb9c31..6298cb5a120 100644 --- a/ckan/templates/group/index.html +++ b/ckan/templates/group/index.html @@ -15,25 +15,25 @@ {% endblock %} {% 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 %} - {% endblock %} - {% block groups_list %} - {% if c.page.items or request.params %} - {% snippet "group/snippets/group_list.html", groups=c.page.items %} - {% else %} -

- {{ _('There are currently no groups for this site') }}. - {% if h.check_access('group_create') %} - {% link_for _('How about creating one?'), controller='group', action='new' %}. - {% endif %} -

- {% endif %} - {% endblock %} - {% block page_pagination %} +

{{ _('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 %} + {% endblock %} + {% block groups_list %} + {% if c.page.items or request.params %} + {% snippet "group/snippets/group_list.html", groups=c.page.items %} + {% else %} +

+ {{ _('There are currently no groups for this site') }}. + {% if h.check_access('group_create') %} + {% link_for _('How about creating one?'), controller='group', action='new' %}. + {% endif %} +

+ {% endif %} + {% endblock %} + {% block page_pagination %} {{ c.page.pager() }} - {% endblock %} + {% endblock %} {% endblock %} {% block secondary_content %} From ac467291fae240f907cdbc1615c5cc45d499e1ed Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:21:30 +0100 Subject: [PATCH 085/189] [#858] Whitespace cleanup ckan/templates/group/member_new.html --- ckan/templates/group/member_new.html | 44 ++++++++++++++-------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/ckan/templates/group/member_new.html b/ckan/templates/group/member_new.html index 87c6dbb60c2..14e281d75b2 100644 --- a/ckan/templates/group/member_new.html +++ b/ckan/templates/group/member_new.html @@ -10,31 +10,31 @@

{% block page_heading %}{{ _('Edit Member') if user else _('Add Member') }}{% endblock %}

{% block form %} -
- {% if user %} - - {% set format_attrs = {'disabled': true} %} - {{ form.input('username', label=_('User'), value=user.name, classes=['control-medium'], attrs=format_attrs) }} - {% else %} - {% set format_attrs = {'data-module': 'autocomplete', 'data-module-source': '/api/2/util/user/autocomplete?q=?'} %} - {{ form.input('username', id='field-username', label=_('User'), placeholder=_('Username'), value='', error='', classes=['control-medium'], attrs=format_attrs) }} - {% endif %} - {% set format_attrs = {'data-module': 'autocomplete'} %} - {{ form.select('role', label=_('Role'), options=c.roles, selected=c.user_role, error='', attrs=format_attrs) }} -
+ {% if user %} - {% set locale = h.dump_json({'content': _('Are you sure you want to delete this member?')}) %} - {{ _('Delete') }} - + + {% set format_attrs = {'disabled': true} %} + {{ form.input('username', label=_('User'), value=user.name, classes=['control-medium'], attrs=format_attrs) }} {% else %} - + {% set format_attrs = {'data-module': 'autocomplete', 'data-module-source': '/api/2/util/user/autocomplete?q=?'} %} + {{ form.input('username', id='field-username', label=_('User'), placeholder=_('Username'), value='', error='', classes=['control-medium'], attrs=format_attrs) }} {% endif %} -
-
+ {% set format_attrs = {'data-module': 'autocomplete'} %} + {{ form.select('role', label=_('Role'), options=c.roles, selected=c.user_role, error='', attrs=format_attrs) }} +
+ {% if user %} + {% set locale = h.dump_json({'content': _('Are you sure you want to delete this member?')}) %} + {{ _('Delete') }} + + {% else %} + + {% endif %} +
+ {% endblock %} {% endblock %} From 461e7d65e27b6daf59cf06a9bdcc9a91dc68ec89 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:21:43 +0100 Subject: [PATCH 086/189] [#858] Whitespaces cleanup ckan/templates/group/new.html --- ckan/templates/group/new.html | 1 - 1 file changed, 1 deletion(-) diff --git a/ckan/templates/group/new.html b/ckan/templates/group/new.html index 91179be106c..23d66facb05 100644 --- a/ckan/templates/group/new.html +++ b/ckan/templates/group/new.html @@ -11,4 +11,3 @@ {% block secondary_content %} {% snippet "group/snippets/helper.html" %} {% endblock %} - From 7e82da910a369b0d2a673a6a19a653806864d967 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:22:05 +0100 Subject: [PATCH 087/189] [#858] Whitespaces cleanup ckan/templates/group/read.html --- ckan/templates/group/read.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ckan/templates/group/read.html b/ckan/templates/group/read.html index 6d50fd0d905..8218edb4b37 100644 --- a/ckan/templates/group/read.html +++ b/ckan/templates/group/read.html @@ -18,9 +18,9 @@ {% snippet 'snippets/search_form.html', type='dataset', query=c.q, sorting_selected=c.sort_by_selected, count=c.page.item_count, facets=facets, placeholder=_('Search datasets...'), show_empty=request.params %} {% endblock %} {% block packages_list %} - {% if c.page.items %} - {{ h.snippet('snippets/package_list.html', packages=c.page.items) }} - {% endif %} + {% if c.page.items %} + {{ h.snippet('snippets/package_list.html', packages=c.page.items) }} + {% endif %} {% endblock %} {% block page_pagination %} {{ c.page.pager(q=c.q) }} From 3b593d8490460ab7f45d8ec08e7f95c81d6cf43f Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:22:57 +0100 Subject: [PATCH 088/189] [#858] Whitespaces cleanup ckan/templates/home/index.html --- ckan/templates/home/index.html | 45 +++++++++++++++++----------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/ckan/templates/home/index.html b/ckan/templates/home/index.html index 2508f8b8508..324a607ec0b 100644 --- a/ckan/templates/home/index.html +++ b/ckan/templates/home/index.html @@ -55,29 +55,29 @@

{% block home_image_caption %}{{ _("This is a featured
{% block home_secondary_content %} -
- {% block home_search %} -
-

{{ _("Search Your Data") }}

-
- - -
-
- {% endblock %} - {% block home_tags %} -
-

{{ _('Popular Tags') }}

- {% set tags = h.get_facet_items_dict('tags', limit=3) %} - {% for tag in tags %} - {{ h.truncate(tag.display_name, 22) }} - {% endfor %} +
+ {% block home_search %} +
+

{{ _("Search Your Data") }}

+
+ +
- {% endblock %} -
+ + {% endblock %} + {% block home_tags %} +
+

{{ _('Popular Tags') }}

+ {% set tags = h.get_facet_items_dict('tags', limit=3) %} + {% for tag in tags %} + {{ h.truncate(tag.display_name, 22) }} + {% endfor %} +
+ {% endblock %} +
{% endblock %}
@@ -96,5 +96,4 @@

{{ _('Popular Tags') }}

{% endblock %} {# Remove the toolbar. #} - {% block toolbar %}{% endblock %} From 785c5f4f770f9ad8536d44460a4beba6c85b0bbb Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:23:26 +0100 Subject: [PATCH 089/189] [#858] Whitespaces cleanup ckan/templates/macros/autoform.html --- ckan/templates/macros/autoform.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ckan/templates/macros/autoform.html b/ckan/templates/macros/autoform.html index 68460c3d6a7..8a24c7667fe 100644 --- a/ckan/templates/macros/autoform.html +++ b/ckan/templates/macros/autoform.html @@ -46,11 +46,11 @@ {% if item.extra_info %}{{ form.info(item.extra_info) }}{% endif %} {% endcall %} {% elif control == 'html' %} -
-
- {{ item.html }} +
+
+ {{ item.html }} +
-
{% else %} {% call form[control](name, id=id, label=label, placeholder=placeholder, value=value, error=error, classes=classes) %} {% if item.extra_info %}{{ form.info(item.extra_info) }}{% endif %} From 9f6a5e07f4acb8dd44b34343759ff51edc3b1f10 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:28:36 +0100 Subject: [PATCH 090/189] [#858] Whitespace cleanup ckan/templates/organization/about.html --- ckan/templates/organization/about.html | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/ckan/templates/organization/about.html b/ckan/templates/organization/about.html index 47b56d5ebdd..6848ed9ede7 100644 --- a/ckan/templates/organization/about.html +++ b/ckan/templates/organization/about.html @@ -3,16 +3,15 @@ {% block subtitle %}{{ _('About') }} - {{ c.group_dict.display_name }}{% endblock %} {% block primary_content_inner %} -

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

- {% block organization_description %} - {% if c.group_dict.description %} - {{ h.render_markdown(c.group_dict.description) }} - {% else %} -

{{ _('There is no description for this organization') }}

- {% endif %} - {% endblock %} - - {% block organization_extras %} - {% snippet 'snippets/additional_info.html', extras = h.sorted_extras(c.group_dict.extras) %} - {% endblock %} +

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

+ {% block organization_description %} + {% if c.group_dict.description %} + {{ h.render_markdown(c.group_dict.description) }} + {% else %} +

{{ _('There is no description for this organization') }}

+ {% endif %} + {% endblock %} + {% block organization_extras %} + {% snippet 'snippets/additional_info.html', extras = h.sorted_extras(c.group_dict.extras) %} + {% endblock %} {% endblock %} From 39ee2eb23e5db40bdf96fe20f01b0484e5bf6554 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:29:06 +0100 Subject: [PATCH 091/189] [#858] Whitespace cleanup ckan/templates/organization/activity_stream.html --- ckan/templates/organization/activity_stream.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ckan/templates/organization/activity_stream.html b/ckan/templates/organization/activity_stream.html index 0320bb76c16..dc8e711c5c8 100644 --- a/ckan/templates/organization/activity_stream.html +++ b/ckan/templates/organization/activity_stream.html @@ -3,8 +3,8 @@ {% block subtitle %}{{ _('Activity Stream') }} - {{ c.group_dict.title or c.group_dict.name }}{% endblock %} {% block primary_content_inner %} -

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

- {% block activity_stream %} - {{ c.group_activity_stream | safe }} - {% endblock %} +

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

+ {% block activity_stream %} + {{ c.group_activity_stream | safe }} + {% endblock %} {% endblock %} From 4e4c49a2812dc2a2ff87f4c6daf92d0c1170c738 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:29:23 +0100 Subject: [PATCH 092/189] [#858] Whitespace cleanup ckan/templates/organization/admins.html --- ckan/templates/organization/admins.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ckan/templates/organization/admins.html b/ckan/templates/organization/admins.html index fa9a1cdbeeb..034fdb68ddb 100644 --- a/ckan/templates/organization/admins.html +++ b/ckan/templates/organization/admins.html @@ -3,8 +3,8 @@ {% block subtitle %}{{ _('Administrators') }} - {{ c.group_dict.title or c.group_dict.name }}{% endblock %} {% block primary_content_inner %} -

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

- {% block admins_list %} +

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

+ {% block admins_list %} {% snippet "user/snippets/followers.html", followers=c.admins %} - {% endblock %} + {% endblock %} {% endblock %} From 891e401126b76549339d2873fef02b397374775e Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:29:37 +0100 Subject: [PATCH 093/189] [#858] Whitespace cleanup ckan/templates/organization/base_form_page.html --- ckan/templates/organization/base_form_page.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ckan/templates/organization/base_form_page.html b/ckan/templates/organization/base_form_page.html index 4eb308287e1..021380ad513 100644 --- a/ckan/templates/organization/base_form_page.html +++ b/ckan/templates/organization/base_form_page.html @@ -1,10 +1,10 @@ {% extends "organization/edit_base.html" %} {% block primary_content_inner %} -

- {% block page_heading %}{{ _('Organization Form') }}{% endblock %} -

- {% block form %} - {{ c.form | safe }} - {% endblock %} +

+ {% block page_heading %}{{ _('Organization Form') }}{% endblock %} +

+ {% block form %} + {{ c.form | safe }} + {% endblock %} {% endblock %} From 99a29e6a9070b95e1f14dae97f23eddf3fc0a39d Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:29:51 +0100 Subject: [PATCH 094/189] [#858] Whitespace cleanup ckan/templates/organization/bulk_process.html --- ckan/templates/organization/bulk_process.html | 172 +++++++++--------- 1 file changed, 86 insertions(+), 86 deletions(-) diff --git a/ckan/templates/organization/bulk_process.html b/ckan/templates/organization/bulk_process.html index f06809fcd07..13c2001e45c 100644 --- a/ckan/templates/organization/bulk_process.html +++ b/ckan/templates/organization/bulk_process.html @@ -12,101 +12,101 @@

{{ _('Edit datasets') }}

{% block page_heading %} - {%- if c.page.item_count -%} - {{ c.page.item_count }} datasets{{ _(" found for \"{query}\"").format(query=c.q) if c.q }} - {%- elif request.params -%} - {{ _('Sorry no datasets found for "{query}"').format(query=c.q) }} - {%- else -%} - {{ _('Datasets') }} - {%- endif -%} + {%- if c.page.item_count -%} + {{ c.page.item_count }} datasets{{ _(" found for \"{query}\"").format(query=c.q) if c.q }} + {%- elif request.params -%} + {{ _('Sorry no datasets found for "{query}"').format(query=c.q) }} + {%- else -%} + {{ _('Datasets') }} + {%- endif -%} {% endblock %}

{% block form %} - {% if c.page.item_count %} -
- - - - - - - - - - - {% for package in c.packages %} - {% set truncate = truncate or 180 %} - {% set truncate_title = truncate_title or 80 %} - {% set title = package.title or package.name %} - {% set notes = h.markdown_extract(package.notes, extract_length=truncate) %} - - - - - {% endfor %} - -
-
- - -
-
- -
-
- - - - {{ _('Edit') }} - -

- {{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='package', action='read', id=package.name)) }} - {% if package.get('state', '').startswith('draft') %} - {{ _('Draft') }} - {% elif package.get('state', '').startswith('deleted') %} - {{ _('Deleted') }} - {% endif %} - {% if package.private %} - {{ _('Private') }} - {% endif %} -

- {% if notes %} -

{{ notes|urlize }}

- {% else %} -

{{ _("This dataset has no description") }}

- {% endif %} -
-
- {% else %} -

{{ _('This organization has no datasets associated to it') }}

- {% endif %} + {% if c.page.item_count %} +
+ + + + + + + + + + + {% for package in c.packages %} + {% set truncate = truncate or 180 %} + {% set truncate_title = truncate_title or 80 %} + {% set title = package.title or package.name %} + {% set notes = h.markdown_extract(package.notes, extract_length=truncate) %} + + + + + {% endfor %} + +
+
+ + +
+
+ +
+
+ + + + {{ _('Edit') }} + +

+ {{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='package', action='read', id=package.name)) }} + {% if package.get('state', '').startswith('draft') %} + {{ _('Draft') }} + {% elif package.get('state', '').startswith('deleted') %} + {{ _('Deleted') }} + {% endif %} + {% if package.private %} + {{ _('Private') }} + {% endif %} +

+ {% if notes %} +

{{ notes|urlize }}

+ {% else %} +

{{ _("This dataset has no description") }}

+ {% endif %} +
+
+ {% else %} +

{{ _('This organization has no datasets associated to it') }}

+ {% endif %} {% endblock %}
From 6569cbb8341060909acd56c6205f6825801f7a3a Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:30:05 +0100 Subject: [PATCH 095/189] [#858] Whitespace cleanup ckan/templates/organization/confirm_delete.html --- ckan/templates/organization/confirm_delete.html | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ckan/templates/organization/confirm_delete.html b/ckan/templates/organization/confirm_delete.html index 3172036d7aa..a8aca943f93 100644 --- a/ckan/templates/organization/confirm_delete.html +++ b/ckan/templates/organization/confirm_delete.html @@ -8,13 +8,13 @@
{% block form %} -

{{ _('Are you sure you want to delete organization - {name}?').format(name=c.group_dict.name) }}

-

-

- - -
-

+

{{ _('Are you sure you want to delete organization - {name}?').format(name=c.group_dict.name) }}

+

+

+ + +
+

{% endblock %}
From e6b3061439a06b907e387dc613518dc647ceb3e5 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:30:19 +0100 Subject: [PATCH 096/189] [#858] Whitespace cleanup ckan/templates/organization/confirm_delete_member.html --- .../organization/confirm_delete_member.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ckan/templates/organization/confirm_delete_member.html b/ckan/templates/organization/confirm_delete_member.html index 4dd84a4c051..1154691c111 100644 --- a/ckan/templates/organization/confirm_delete_member.html +++ b/ckan/templates/organization/confirm_delete_member.html @@ -8,14 +8,14 @@
{% block form %} -

{{ _('Are you sure you want to delete member - {name}?').format(name=c.user_dict.name) }}

-

-

- - - -
-

+

{{ _('Are you sure you want to delete member - {name}?').format(name=c.user_dict.name) }}

+

+

+ + + +
+

{% endblock %}
From 3c2fddc5e6476f610d86c634ccd32615c32b92b3 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:30:31 +0100 Subject: [PATCH 097/189] [#858] Whitespace cleanup ckan/templates/organization/index.html --- ckan/templates/organization/index.html | 36 +++++++++++++------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/ckan/templates/organization/index.html b/ckan/templates/organization/index.html index 032a8c7d0da..ae2fb2bc49b 100644 --- a/ckan/templates/organization/index.html +++ b/ckan/templates/organization/index.html @@ -15,25 +15,25 @@ {% endblock %} {% 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 %} - {% endblock %} - {% block organizations_list %} - {% if c.page.items or request.params %} - {% snippet "organization/snippets/organization_list.html", organizations=c.page.items %} - {% else %} -

- {{ _('There are currently no organizations for this site') }}. - {% if h.check_access('organization_create') %} - {% link_for _('How about creating one?'), controller='organization', action='new' %}. - {% endif %} -

- {% endif %} - {% endblock %} - {% block page_pagination %} +

{% 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 %} + {% endblock %} + {% block organizations_list %} + {% if c.page.items or request.params %} + {% snippet "organization/snippets/organization_list.html", organizations=c.page.items %} + {% else %} +

+ {{ _('There are currently no organizations for this site') }}. + {% if h.check_access('organization_create') %} + {% link_for _('How about creating one?'), controller='organization', action='new' %}. + {% endif %} +

+ {% endif %} + {% endblock %} + {% block page_pagination %} {{ c.page.pager() }} - {% endblock %} + {% endblock %} {% endblock %} {% block secondary_content %} From 1a1cc9b9c8ab49249a9c01a14c36b009520505e6 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:30:47 +0100 Subject: [PATCH 098/189] [#858] Whitespace cleanup ckan/templates/organization/members.html --- ckan/templates/organization/members.html | 30 ++++++++++++------------ 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/ckan/templates/organization/members.html b/ckan/templates/organization/members.html index 875263feaa3..c5dcd0ebdae 100644 --- a/ckan/templates/organization/members.html +++ b/ckan/templates/organization/members.html @@ -21,21 +21,21 @@

{{ _('{0} members'.format(c.members|length)) }}

{% for user_id, user, role in c.members %} - - - {{ h.linked_user(user_id, maxlength=20) }} - - {{ role }} - - {% set locale = h.dump_json({'content': _('Are you sure you want to delete this member?')}) %} - - - + + + {{ h.linked_user(user_id, maxlength=20) }} + + {{ role }} + + {% set locale = h.dump_json({'content': _('Are you sure you want to delete this member?')}) %} + + + {% endfor %} From 43fc5a8e313cae21389ddcf39550ca0306ae8a55 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:31:01 +0100 Subject: [PATCH 099/189] [#858] Whitespace cleanup ckan/templates/organization/read.html --- ckan/templates/organization/read.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ckan/templates/organization/read.html b/ckan/templates/organization/read.html index aae8f995b20..a702541fd66 100644 --- a/ckan/templates/organization/read.html +++ b/ckan/templates/organization/read.html @@ -18,9 +18,9 @@ {% snippet 'snippets/search_form.html', type='dataset', query=c.q, sorting_selected=c.sort_by_selected, count=c.page.item_count, facets=facets, placeholder=_('Search datasets...'), show_empty=request.params %} {% endblock %} {% block packages_list %} - {% if c.page.items %} - {{ h.snippet('snippets/package_list.html', packages=c.page.items) }} - {% endif %} + {% if c.page.items %} + {{ h.snippet('snippets/package_list.html', packages=c.page.items) }} + {% endif %} {% endblock %} {% block page_pagination %} {{ c.page.pager(q=c.q) }} @@ -29,6 +29,6 @@ {% block organization_facets %} {% for facet in c.facet_titles %} - {{ h.snippet('snippets/facet_list.html', title=c.facet_titles[facet], name=facet, extras={'id':c.group_dict.id}) }} + {{ h.snippet('snippets/facet_list.html', title=c.facet_titles[facet], name=facet, extras={'id':c.group_dict.id}) }} {% endfor %} {% endblock %} From a55de7e4db497d8579b48ebd4a112651c98371e3 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:31:16 +0100 Subject: [PATCH 100/189] [#858] Whitespace cleanup ckan/templates/organization/read_base.html --- ckan/templates/organization/read_base.html | 1 - 1 file changed, 1 deletion(-) diff --git a/ckan/templates/organization/read_base.html b/ckan/templates/organization/read_base.html index a9ff1bf6302..d5a0f8120d5 100644 --- a/ckan/templates/organization/read_base.html +++ b/ckan/templates/organization/read_base.html @@ -21,7 +21,6 @@ {% block secondary_content %} {% snippet 'snippets/organization.html', organization=c.group_dict, show_nums=true %} - {% block organization_facets %}{% endblock %} {% endblock %} From 8e0254779584af504d282788278eb30d08f1adb1 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:52 +0100 Subject: [PATCH 101/189] [#858] Whitespace cleanup ckan/templates/footer.html --- ckan/templates/footer.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/templates/footer.html b/ckan/templates/footer.html index 7c6dc9a4d2b..01248a5d4cd 100644 --- a/ckan/templates/footer.html +++ b/ckan/templates/footer.html @@ -20,7 +20,7 @@
{% block footer_attribution %} -

{% trans %}Powered by {% endtrans %}

+

{% trans %}Powered by {% endtrans %}

{% endblock %} {% block footer_lang %} {% snippet "snippets/language_selector.html" %} From 3573376720e060aebee4d3e949710081a3df933c Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:52 +0100 Subject: [PATCH 102/189] [#858] Whitespace cleanup ckan/templates/header.html --- ckan/templates/header.html | 86 +++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/ckan/templates/header.html b/ckan/templates/header.html index 5b4ebe225a7..336ba3a781d 100644 --- a/ckan/templates/header.html +++ b/ckan/templates/header.html @@ -6,41 +6,41 @@ @@ -48,8 +48,8 @@ @@ -59,9 +59,9 @@ {% endblock %}
{% block header_debug %} - {% if g.debug and not g.debug_supress_header %} -
Controller : {{ c.controller }}
Action : {{ c.action }}
- {% endif %} + {% if g.debug and not g.debug_supress_header %} +
Controller : {{ c.controller }}
Action : {{ c.action }}
+ {% endif %} {% endblock %}
{# The .header-image class hides the main text and uses image replacement for the title #} @@ -86,12 +86,12 @@

From 75cbb3c1e70b128973c19e62824fd98f50991d49 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:52 +0100 Subject: [PATCH 103/189] [#858] Whitespace cleanup ckan/templates/package/activity.html --- ckan/templates/package/activity.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ckan/templates/package/activity.html b/ckan/templates/package/activity.html index 14600d5211a..52c0f8eade7 100644 --- a/ckan/templates/package/activity.html +++ b/ckan/templates/package/activity.html @@ -3,8 +3,8 @@ {% block subtitle %}{{ _('Activity Stream') }} - {{ c.pkg_dict.title or c.pkg_dict.name }}{% endblock %} {% block primary_content_inner %} -

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

- {% block activity_stream %} +

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

+ {% block activity_stream %} {{ c.package_activity_stream | safe }} - {% endblock %} + {% endblock %} {% endblock %} From fa1fcb424a8814994684f09d3d637f10fae7c8b9 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:52 +0100 Subject: [PATCH 104/189] [#858] Whitespace cleanup ckan/templates/package/activity_stream.html --- ckan/templates/package/activity_stream.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ckan/templates/package/activity_stream.html b/ckan/templates/package/activity_stream.html index 233c3b58c43..af67b063a7b 100644 --- a/ckan/templates/package/activity_stream.html +++ b/ckan/templates/package/activity_stream.html @@ -3,8 +3,8 @@ {% block subtitle %}{{ _('Activity Stream') }}{% endblock %} {% block package_content %} -

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

- {% block activity_stream %} - {{ c.package_activity_stream | safe }} - {% endblock %} +

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

+ {% block activity_stream %} + {{ c.package_activity_stream | safe }} + {% endblock %} {% endblock %} From 9a01b59c7ddb717581e5fe6bc82bee21324a8156 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:52 +0100 Subject: [PATCH 105/189] [#858] Whitespace cleanup ckan/templates/package/confirm_delete.html --- ckan/templates/package/confirm_delete.html | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ckan/templates/package/confirm_delete.html b/ckan/templates/package/confirm_delete.html index 798e2786851..7788af265f1 100644 --- a/ckan/templates/package/confirm_delete.html +++ b/ckan/templates/package/confirm_delete.html @@ -8,13 +8,13 @@
{% block form %} -

{{ _('Are you sure you want to delete dataset - {name}?').format(name=c.pkg_dict.name) }}

-

-

- - -
-

+

{{ _('Are you sure you want to delete dataset - {name}?').format(name=c.pkg_dict.name) }}

+

+

+ + +
+

{% endblock %}
From d0e73099d69882714ee5f4798f6c21438c2a9759 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:52 +0100 Subject: [PATCH 106/189] [#858] Whitespace cleanup ckan/templates/package/confirm_delete_resource.html --- .../templates/package/confirm_delete_resource.html | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ckan/templates/package/confirm_delete_resource.html b/ckan/templates/package/confirm_delete_resource.html index 02e7ccef313..6a4c9083307 100644 --- a/ckan/templates/package/confirm_delete_resource.html +++ b/ckan/templates/package/confirm_delete_resource.html @@ -8,13 +8,13 @@
{% block form %} -

{{ _('Are you sure you want to delete resource - {name}?').format(name=h.resource_display_name(c.resource_dict)) }}

-

-

- - -
-

+

{{ _('Are you sure you want to delete resource - {name}?').format(name=h.resource_display_name(c.resource_dict)) }}

+

+

+ + +
+

{% endblock %}
From a049f61e7fb815e76f676578c7566a4f1cbe254c Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:52 +0100 Subject: [PATCH 107/189] [#858] Whitespace cleanup ckan/templates/package/followers.html --- ckan/templates/package/followers.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ckan/templates/package/followers.html b/ckan/templates/package/followers.html index 04211893fde..c6da2372ef0 100644 --- a/ckan/templates/package/followers.html +++ b/ckan/templates/package/followers.html @@ -3,8 +3,8 @@ {% block subtitle %}{{ _('Followers') }} - {{ c.pkg_dict.title or c.pkg_dict.name }}{% endblock %} {% block primary_content_inner %} -

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

- {% block followers_list %} +

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

+ {% block followers_list %} {% snippet "user/snippets/followers.html", followers=c.followers %} - {% endblock %} + {% endblock %} {% endblock %} From aab982342dd34893a55e9a5978df3a20b87b3ab5 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:52 +0100 Subject: [PATCH 108/189] [#858] Whitespace cleanup ckan/templates/package/history.html --- ckan/templates/package/history.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ckan/templates/package/history.html b/ckan/templates/package/history.html index f4e3516cc96..8b499f39029 100644 --- a/ckan/templates/package/history.html +++ b/ckan/templates/package/history.html @@ -3,8 +3,8 @@ {% block subtitle %}{{ _('History') }} - {{ c.pkg_dict.title or c.pkg_dict.name }}{% endblock %} {% block primary_content_inner %} -

{{ _('History') }}

- {% block package_history_revisions %} - {% snippet "package/snippets/history_revisions.html", pkg_dict=pkg, pkg_revisions=c.pkg_revisions %} - {% endblock %} +

{{ _('History') }}

+ {% block package_history_revisions %} + {% snippet "package/snippets/history_revisions.html", pkg_dict=pkg, pkg_revisions=c.pkg_revisions %} + {% endblock %} {% endblock %} From 53ef91fc04cc0297a4cca3b3b88475c6b9a1a242 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:52 +0100 Subject: [PATCH 109/189] [#858] Whitespace cleanup ckan/templates/package/read.html --- ckan/templates/package/read.html | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/ckan/templates/package/read.html b/ckan/templates/package/read.html index a805b4a5763..d70cc3e5bf2 100644 --- a/ckan/templates/package/read.html +++ b/ckan/templates/package/read.html @@ -15,18 +15,18 @@ {% endif %}

{% block page_heading %} - {{ pkg.title or pkg.name }} - {% if pkg.state.startswith('draft') %} - [{{ _('Draft') }}] - {% endif %} + {{ pkg.title or pkg.name }} + {% if pkg.state.startswith('draft') %} + [{{ _('Draft') }}] + {% endif %} {% endblock %}

{% block package_notes %} - {% if c.pkg_notes_formatted %} -
- {{ c.pkg_notes_formatted }} -
- {% endif %} + {% if c.pkg_notes_formatted %} +
+ {{ c.pkg_notes_formatted }} +
+ {% endif %} {% endblock %} {# FIXME why is this here? seems wrong #} From 6602a9f9a02bec5842b2acc8ac76ff925f862925 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:52 +0100 Subject: [PATCH 110/189] [#858] Whitespace cleanup ckan/templates/package/read_base.html --- ckan/templates/package/read_base.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ckan/templates/package/read_base.html b/ckan/templates/package/read_base.html index daecd18bf75..9cf73a5c5a1 100644 --- a/ckan/templates/package/read_base.html +++ b/ckan/templates/package/read_base.html @@ -3,8 +3,8 @@ {% block subtitle %}{{ pkg.title or pkg.name }}{% endblock %} {% block links -%} - {{ super() }} - + {{ super() }} + {% endblock -%} {% block head_extras -%} From cefea4a194a44eedc0568b857aead6562469c180 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:52 +0100 Subject: [PATCH 111/189] [#858] Whitespace cleanup ckan/templates/package/related_list.html --- ckan/templates/package/related_list.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ckan/templates/package/related_list.html b/ckan/templates/package/related_list.html index d9aa5ac34f9..0f31cb0e790 100644 --- a/ckan/templates/package/related_list.html +++ b/ckan/templates/package/related_list.html @@ -3,17 +3,17 @@ {% set pkg = c.pkg %} {% block primary_content_inner %} -

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

- {% block related_list %} +

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

+ {% block related_list %} {% if c.pkg.related %} {% snippet "related/snippets/related_list.html", related_items=c.pkg.related, pkg_id=c.pkg.name %} {% else %} -

{{ _('No related items') }}

+

{{ _('No related items') }}

{% endif %} - {% endblock %} - {% block form_actions %} + {% endblock %} + {% block form_actions %}
{% link_for _('Add Related Item'), controller='related', action='new', id=pkg.name, class_='btn btn-primary' %}
- {% endblock %} + {% endblock %} {% endblock %} From 82677c2be5358e3a5ed6c8ca4554f9381f00b458 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:53 +0100 Subject: [PATCH 112/189] [#858] Whitespace cleanup ckan/templates/package/search.html --- ckan/templates/package/search.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ckan/templates/package/search.html b/ckan/templates/package/search.html index 7bf773bd82e..6274ecc0798 100644 --- a/ckan/templates/package/search.html +++ b/ckan/templates/package/search.html @@ -35,12 +35,12 @@ {% snippet 'snippets/search_form.html', type='dataset', query=c.q, sorting=sorting, sorting_selected=c.sort_by_selected, count=c.page.item_count, facets=facets, show_empty=request.params, error=c.query_error %} {% endblock %} {% block package_search_results_list %} - {{ h.snippet('snippets/package_list.html', packages=c.page.items) }} + {{ h.snippet('snippets/package_list.html', packages=c.page.items) }} {% endblock %}

{% block page_pagination %} - {{ c.page.pager(q=c.q) }} + {{ c.page.pager(q=c.q) }} {% endblock %} @@ -68,6 +68,6 @@ {% block secondary_content %} {% for facet in c.facet_titles %} - {{ h.snippet('snippets/facet_list.html', title=c.facet_titles[facet], name=facet) }} + {{ h.snippet('snippets/facet_list.html', title=c.facet_titles[facet], name=facet) }} {% endfor %} {% endblock %} From ef89631998eaef68b9a19efb28f0670b1c992fe9 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:53 +0100 Subject: [PATCH 113/189] [#858] Whitespace cleanup ckan/templates/package/snippets/additional_info.html --- .../package/snippets/additional_info.html | 92 +++++++++---------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/ckan/templates/package/snippets/additional_info.html b/ckan/templates/package/snippets/additional_info.html index 2fa73a36ec2..7bfdf3d0f39 100644 --- a/ckan/templates/package/snippets/additional_info.html +++ b/ckan/templates/package/snippets/additional_info.html @@ -9,57 +9,57 @@

{{ _('Additional Info') }}

{% block package_additional_info %} - {% if pkg_dict.url %} - - {{ _('Source') }} - {{ h.link_to(pkg_dict.url, pkg_dict.url, rel='foaf:homepage', target='_blank') }} - - {% endif %} + {% if pkg_dict.url %} + + {{ _('Source') }} + {{ h.link_to(pkg_dict.url, pkg_dict.url, rel='foaf:homepage', target='_blank') }} + + {% endif %} - {% if pkg_dict.author_email %} - - {{ _("Author") }} - {{ h.mail_to(email_address=pkg_dict.author_email, name=pkg_dict.author) }} - - {% elif pkg_dict.author %} - - {{ _("Author") }} - {{ pkg_dict.author }} - - {% endif %} + {% if pkg_dict.author_email %} + + {{ _("Author") }} + {{ h.mail_to(email_address=pkg_dict.author_email, name=pkg_dict.author) }} + + {% elif pkg_dict.author %} + + {{ _("Author") }} + {{ pkg_dict.author }} + + {% endif %} - {% if pkg_dict.maintainer_email %} - - {{ _('Maintainer') }} - {{ h.mail_to(email_address=pkg_dict.maintainer_email, name=pkg_dict.maintainer) }} - - {% elif pkg_dict.maintainer %} - - {{ _('Maintainer') }} - {{ pkg_dict.maintainer }} - - {% endif %} + {% if pkg_dict.maintainer_email %} + + {{ _('Maintainer') }} + {{ h.mail_to(email_address=pkg_dict.maintainer_email, name=pkg_dict.maintainer) }} + + {% elif pkg_dict.maintainer %} + + {{ _('Maintainer') }} + {{ pkg_dict.maintainer }} + + {% endif %} - {% if pkg_dict.version %} - - {{ _("Version") }} - {{ pkg_dict.version }} - - {% endif %} + {% if pkg_dict.version %} + + {{ _("Version") }} + {{ pkg_dict.version }} + + {% endif %} - {% if h.check_access('package_update',{'id':pkg_dict.id}) %} - - {{ _("State") }} - {{ pkg_dict.state }} - - {% endif %} + {% if h.check_access('package_update',{'id':pkg_dict.id}) %} + + {{ _("State") }} + {{ pkg_dict.state }} + + {% endif %} - {% for extra in h.sorted_extras(pkg_dict.extras) %} - {% set key, value = extra %} - - {{ _(key) }} - {{ value }} - + {% for extra in h.sorted_extras(pkg_dict.extras) %} + {% set key, value = extra %} + + {{ _(key) }} + {{ value }} + {% endfor %} {% endblock %} From 4c2376ad6ecae43b14e40c81ad343fdb2aa6127c Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:53 +0100 Subject: [PATCH 114/189] [#858] Whitespace cleanup ckan/templates/package/snippets/info.html --- ckan/templates/package/snippets/info.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ckan/templates/package/snippets/info.html b/ckan/templates/package/snippets/info.html index 704cc8225bf..49efa7b8fc7 100644 --- a/ckan/templates/package/snippets/info.html +++ b/ckan/templates/package/snippets/info.html @@ -22,13 +22,13 @@

{{ _("Edit Dataset") }}<

{{ _("Edit Resources") }}

-{% endif %} \ No newline at end of file +{% endif %} From 857a663455d03460cba198de921360abc4dc0277 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:53 +0100 Subject: [PATCH 115/189] [#858] Whitespace cleanup ckan/templates/package/snippets/package_basic_fields.html --- .../snippets/package_basic_fields.html | 109 +++++++++--------- 1 file changed, 54 insertions(+), 55 deletions(-) diff --git a/ckan/templates/package/snippets/package_basic_fields.html b/ckan/templates/package/snippets/package_basic_fields.html index 9a85d41a421..8c224a7e21d 100644 --- a/ckan/templates/package/snippets/package_basic_fields.html +++ b/ckan/templates/package/snippets/package_basic_fields.html @@ -1,28 +1,27 @@ {% import 'macros/form.html' as form %} {% block package_basic_fields_title %} -{{ form.input('title', id='field-title', label=_('Title'), placeholder=_('eg. A descriptive title'), value=data.title, error=errors.title, classes=['control-full', 'control-large'], attrs={'data-module': 'slug-preview-target'}) }} + {{ form.input('title', id='field-title', label=_('Title'), placeholder=_('eg. A descriptive title'), value=data.title, error=errors.title, classes=['control-full', 'control-large'], attrs={'data-module': 'slug-preview-target'}) }} {% endblock %} {% block package_basic_fields_url %} -{% set prefix = h.url_for(controller='package', action='read', id='') %} -{% set domain = h.url_for(controller='package', action='read', id='', qualified=true) %} -{% set domain = domain|replace("http://", "")|replace("https://", "") %} -{% set attrs = {'data-module': 'slug-preview-slug', 'data-module-prefix': domain, 'data-module-placeholder': ''} %} + {% set prefix = h.url_for(controller='package', action='read', id='') %} + {% set domain = h.url_for(controller='package', action='read', id='', qualified=true) %} + {% set domain = domain|replace("http://", "")|replace("https://", "") %} + {% set attrs = {'data-module': 'slug-preview-slug', 'data-module-prefix': domain, 'data-module-placeholder': ''} %} -{{ form.prepend('name', id='field-name', label=_('URL'), prepend=prefix, placeholder=_('eg. my-dataset'), value=data.name, error=errors.name, attrs=attrs, is_required=true) }} + {{ form.prepend('name', id='field-name', label=_('URL'), prepend=prefix, placeholder=_('eg. my-dataset'), value=data.name, error=errors.name, attrs=attrs, is_required=true) }} {% endblock %} -{% block package_basic_fields_custom %} -{% endblock %} +{% block package_basic_fields_custom %}{% endblock %} {% block package_basic_fields_description %} -{{ form.markdown('notes', id='field-notes', label=_('Description'), placeholder=_('eg. Some useful notes about the data'), value=data.notes, error=errors.notes) }} + {{ form.markdown('notes', id='field-notes', label=_('Description'), placeholder=_('eg. Some useful notes about the data'), value=data.notes, error=errors.notes) }} {% endblock %} {% block package_basic_fields_tags %} -{% set tag_attrs = {'data-module': 'autocomplete', 'data-module-tags': '', 'data-module-source': '/api/2/util/tag/autocomplete?incomplete=?'} %} -{{ form.input('tag_string', id='field-tags', label=_('Tags'), placeholder=_('eg. economy, mental health, government'), value=data.tag_string, error=errors.tags, classes=['control-full'], attrs=tag_attrs) }} + {% set tag_attrs = {'data-module': 'autocomplete', 'data-module-tags': '', 'data-module-source': '/api/2/util/tag/autocomplete?incomplete=?'} %} + {{ form.input('tag_string', id='field-tags', label=_('Tags'), placeholder=_('eg. economy, mental health, government'), value=data.tag_string, error=errors.tags, classes=['control-full'], attrs=tag_attrs) }} {% endblock %} {% block package_basic_fields_license %} @@ -48,57 +47,57 @@ {% endblock %} {% block package_basic_fields_org %} -{# if we have a default group then this wants remembering #} -{% if data.group_id %} - -{% endif %} + {# if we have a default group then this wants remembering #} + {% if data.group_id %} + + {% endif %} -{% set dataset_is_draft = data.get('state', 'draft').startswith('draft') or data.get('state', 'none') == 'none' %} -{% set dataset_has_organization = data.owner_org or data.group_id %} -{% set organizations_available = h.organizations_available('create_dataset') %} -{% set user_is_sysadmin = h.check_access('sysadmin') %} -{% set show_organizations_selector = organizations_available and (user_is_sysadmin or dataset_is_draft) %} -{% set show_visibility_selector = dataset_has_organization or (organizations_available and (user_is_sysadmin or dataset_is_draft)) %} + {% set dataset_is_draft = data.get('state', 'draft').startswith('draft') or data.get('state', 'none') == 'none' %} + {% set dataset_has_organization = data.owner_org or data.group_id %} + {% set organizations_available = h.organizations_available('create_dataset') %} + {% set user_is_sysadmin = h.check_access('sysadmin') %} + {% set show_organizations_selector = organizations_available and (user_is_sysadmin or dataset_is_draft) %} + {% set show_visibility_selector = dataset_has_organization or (organizations_available and (user_is_sysadmin or dataset_is_draft)) %} -{% if show_organizations_selector and show_visibility_selector %} -
-{% endif %} + {% if show_organizations_selector and show_visibility_selector %} +
+ {% endif %} -{% if show_organizations_selector %} -
- -
- + {% if show_organizations_selector %} +
+ +
+ +
-
-{% endif %} + {% endif %} -{% if show_visibility_selector %} - {% block package_metadata_fields_visibility %} -
- -
- + {% if show_visibility_selector %} + {% block package_metadata_fields_visibility %} +
+ +
+ +
-
- {% endblock %} -{% endif %} + {% endblock %} + {% endif %} -{% if show_organizations_selector and show_visibility_selector %} -
-{% endif %} + {% if show_organizations_selector and show_visibility_selector %} +
+ {% endif %} -{{ form.required_message() }} + {{ form.required_message() }} {% endblock %} From 945f418814dd7b3a6e70ca8776b5a6a601a7d619 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:53 +0100 Subject: [PATCH 116/189] [#858] Whitespace cleanup ckan/templates/package/snippets/package_metadata_fields.html --- .../snippets/package_metadata_fields.html | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/ckan/templates/package/snippets/package_metadata_fields.html b/ckan/templates/package/snippets/package_metadata_fields.html index fce09b1d8bc..ac841ddd4d0 100644 --- a/ckan/templates/package/snippets/package_metadata_fields.html +++ b/ckan/templates/package/snippets/package_metadata_fields.html @@ -2,24 +2,23 @@ {% block package_metadata_fields %} -{% block package_metadata_author %} -{{ form.input('author', label=_('Author'), id='field-author', placeholder=_('Joe Bloggs'), value=data.author, error=errors.author, classes=['control-medium']) }} + {% block package_metadata_author %} + {{ form.input('author', label=_('Author'), id='field-author', placeholder=_('Joe Bloggs'), value=data.author, error=errors.author, classes=['control-medium']) }} -{{ form.input('author_email', label=_('Author Email'), id='field-author-email', placeholder=_('joe@example.com'), value=data.author_email, error=errors.author_email, classes=['control-medium']) }} -{% endblock %} - -{% block package_metadata_fields_maintainer %} -{{ form.input('maintainer', label=_('Maintainer'), id='field-maintainer', placeholder=_('Joe Bloggs'), value=data.maintainer, error=errors.maintainer, classes=['control-medium']) }} + {{ form.input('author_email', label=_('Author Email'), id='field-author-email', placeholder=_('joe@example.com'), value=data.author_email, error=errors.author_email, classes=['control-medium']) }} + {% endblock %} -{{ form.input('maintainer_email', label=_('Maintainer Email'), id='field-maintainer-email', placeholder=_('joe@example.com'), value=data.maintainer_email, error=errors.maintainer_email, classes=['control-medium']) }} -{% endblock %} + {% block package_metadata_fields_maintainer %} + {{ form.input('maintainer', label=_('Maintainer'), id='field-maintainer', placeholder=_('Joe Bloggs'), value=data.maintainer, error=errors.maintainer, classes=['control-medium']) }} -{% block package_metadata_fields_custom %} -{% block custom_fields %} - {% snippet 'snippets/custom_form_fields.html', extras=data.extras, errors=errors, limit=3 %} -{% endblock %} -{% endblock %} + {{ form.input('maintainer_email', label=_('Maintainer Email'), id='field-maintainer-email', placeholder=_('joe@example.com'), value=data.maintainer_email, error=errors.maintainer_email, classes=['control-medium']) }} + {% endblock %} + {% block package_metadata_fields_custom %} + {% block custom_fields %} + {% snippet 'snippets/custom_form_fields.html', extras=data.extras, errors=errors, limit=3 %} + {% endblock %} + {% endblock %} {% block dataset_fields %} {% if data.groups %} From a5070d5b11d9b397d2adff769e4be7a6e61476ae Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:53 +0100 Subject: [PATCH 117/189] [#858] Whitespace cleanup ckan/templates/package/snippets/resource_form.html --- .../package/snippets/resource_form.html | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/ckan/templates/package/snippets/resource_form.html b/ckan/templates/package/snippets/resource_form.html index 397d3d2504d..f3bb2bddc91 100644 --- a/ckan/templates/package/snippets/resource_form.html +++ b/ckan/templates/package/snippets/resource_form.html @@ -19,44 +19,44 @@
{% 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 {} - #} - - - - - - + {% 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 %} + {% 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) }} + {{ 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) }} {% endblock %} {% block basic_fields_name %} - {{ form.input('name', id='field-name', label=_('Name'), placeholder=_('eg. January 2011 Gold Prices'), value=data.name, error=errors.name, classes=['control-full']) }} + {{ form.input('name', id='field-name', label=_('Name'), placeholder=_('eg. January 2011 Gold Prices'), value=data.name, error=errors.name, classes=['control-full']) }} {% endblock %} {% block basic_fields_description %} - {{ form.markdown('description', id='field-description', label=_('Description'), placeholder=_('Some useful notes about the data'), value=data.description, error=errors.description) }} + {{ form.markdown('description', id='field-description', label=_('Description'), placeholder=_('Some useful notes about the data'), value=data.description, error=errors.description) }} {% endblock %} {% block basic_fields_format %} - {% set format_attrs = {'data-module': 'autocomplete', 'data-module-source': '/api/2/util/resource/format_autocomplete?incomplete=?'} %} - {% call form.input('format', id='field-format', label=_('Format'), placeholder=_('eg. CSV, XML or JSON'), value=data.format, error=errors.format, classes=['control-medium'], attrs=format_attrs) %} - - - {{ _('This is generated automatically. You can edit if you wish') }} - - {% endcall %} + {% set format_attrs = {'data-module': 'autocomplete', 'data-module-source': '/api/2/util/resource/format_autocomplete?incomplete=?'} %} + {% call form.input('format', id='field-format', label=_('Format'), placeholder=_('eg. CSV, XML or JSON'), value=data.format, error=errors.format, classes=['control-medium'], attrs=format_attrs) %} + + + {{ _('This is generated automatically. You can edit if you wish') }} + + {% endcall %} {% endblock %} {{ form.required_message() }} @@ -88,13 +88,13 @@ {% endif %} {% endblock %} {% if stage %} - {% block previous_button %} - - {% endblock %} - {% block again_button %} - - {% endblock %} - + {% block previous_button %} + + {% endblock %} + {% block again_button %} + + {% endblock %} + {% else %} {% endif %} From 634fc8718119c70b0272f878b49110c7179a3046 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:53 +0100 Subject: [PATCH 118/189] [#858] Whitespace cleanup ckan/templates/package/snippets/resources_list.html --- .../package/snippets/resources_list.html | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/ckan/templates/package/snippets/resources_list.html b/ckan/templates/package/snippets/resources_list.html index 7cd33a1c59b..f863508225c 100644 --- a/ckan/templates/package/snippets/resources_list.html +++ b/ckan/templates/package/snippets/resources_list.html @@ -12,21 +12,21 @@

{{ _('Data and Resources') }}

{% block resource_list %} - {% if resources %} -
    - {% block resource_list_inner %} - {% for resource in resources %} - {% snippet 'package/snippets/resource_item.html', pkg=pkg, res=resource %} - {% endfor %} - {% endblock %} -
- {% else %} -

- {# Comment out "add some" as action doesn't exist yet #} - {% trans url=h.url_for(controller='package', action='new_resource', id=pkg.name) %} -

This dataset has no data, why not add some? - {% endtrans %} -

- {% endif %} - {% endblock %} + {% if resources %} +
    + {% block resource_list_inner %} + {% for resource in resources %} + {% snippet 'package/snippets/resource_item.html', pkg=pkg, res=resource %} + {% endfor %} + {% endblock %} +
+ {% else %} +

+ {# Comment out "add some" as action doesn't exist yet #} + {% trans url=h.url_for(controller='package', action='new_resource', id=pkg.name) %} +

This dataset has no data, why not add some? + {% endtrans %} +

+ {% endif %} + {% endblock %}
From 2ad1b5969ec93a372b7524f38e5e402c21ecd24d Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:53 +0100 Subject: [PATCH 119/189] [#858] Whitespace cleanup ckan/templates/package/snippets/revisions_table.html --- ckan/templates/package/snippets/revisions_table.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/templates/package/snippets/revisions_table.html b/ckan/templates/package/snippets/revisions_table.html index e8fc3d40a3e..bd22cac1e8d 100644 --- a/ckan/templates/package/snippets/revisions_table.html +++ b/ckan/templates/package/snippets/revisions_table.html @@ -26,6 +26,6 @@ {{ h.linked_user(rev.author) }} {{ rev.message }} - {% endfor %} + {% endfor %} \ No newline at end of file From cce1d10583cc8ef7da3199297456791664664f8b Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:53 +0100 Subject: [PATCH 120/189] [#858] Whitespace cleanup ckan/templates/page.html --- ckan/templates/page.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/templates/page.html b/ckan/templates/page.html index 0f2b3a57885..8a8485e353b 100644 --- a/ckan/templates/page.html +++ b/ckan/templates/page.html @@ -127,7 +127,7 @@

A sidebar item

{% resource 'base/main' %} {% resource 'base/ckan' %} {% if g.tracking_enabled %} - {% resource 'base/tracking.js' %} + {% resource 'base/tracking.js' %} {% endif %} {{ super() }} {% endblock -%} From 892ba2ad0734c427395c1bd0169f5e56266e0d08 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:53 +0100 Subject: [PATCH 121/189] [#858] Whitespace cleanup ckan/templates/related/base_form_page.html --- ckan/templates/related/base_form_page.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ckan/templates/related/base_form_page.html b/ckan/templates/related/base_form_page.html index 00978865578..cc788a741fb 100644 --- a/ckan/templates/related/base_form_page.html +++ b/ckan/templates/related/base_form_page.html @@ -17,16 +17,16 @@

{% block page_heading %}{{ _('Related Form') }}{% endbl {% block secondary_content %}
-

{{ _('What are related items?') }}

-
- {% trans %} +

{{ _('What are related items?') }}

+
+ {% trans %}

Related Media is any app, article, visualisation or idea related to this dataset.

For example, it could be a custom visualisation, pictograph or bar chart, an app using all or part of the data or even a news story that references this dataset.

- {% endtrans %} + {% endtrans %}
{% endblock %} From c8ded1b68781f47a5fd55deb72b1227e11d6e2b8 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:53 +0100 Subject: [PATCH 122/189] [#858] Whitespace cleanup ckan/templates/related/confirm_delete.html --- ckan/templates/related/confirm_delete.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ckan/templates/related/confirm_delete.html b/ckan/templates/related/confirm_delete.html index 8b95a131798..5c9f82cee9c 100644 --- a/ckan/templates/related/confirm_delete.html +++ b/ckan/templates/related/confirm_delete.html @@ -6,15 +6,15 @@ {% block main_content %}
- {% block form %}
-

{{ _('Are you sure you want to delete related item - {name}?').format(name=c.related_dict.title) }}

-

-

- - -
-

+ {% block form %} +

{{ _('Are you sure you want to delete related item - {name}?').format(name=c.related_dict.title) }}

+

+

+ + +
+

{% endblock %}
From 1508a68da1f84e4633687a318fc4bc5e1a99b077 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:53 +0100 Subject: [PATCH 123/189] [#858] Whitespace cleanup ckan/templates/related/edit_form.html --- ckan/templates/related/edit_form.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/templates/related/edit_form.html b/ckan/templates/related/edit_form.html index 7f08b2b977f..92cb16696e8 100644 --- a/ckan/templates/related/edit_form.html +++ b/ckan/templates/related/edit_form.html @@ -10,6 +10,6 @@ {% block delete_button %} {% if data.id %} - {{ super() }} + {{ super() }} {% endif %} {% endblock %} From a7b01fd1672760f859f70f4530e6a93990bed122 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:53 +0100 Subject: [PATCH 124/189] [#858] Whitespace cleanup ckan/templates/revision/read.html --- ckan/templates/revision/read.html | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ckan/templates/revision/read.html b/ckan/templates/revision/read.html index f291a0ef385..5b9de9c3b58 100644 --- a/ckan/templates/revision/read.html +++ b/ckan/templates/revision/read.html @@ -20,15 +20,15 @@ >
  • {% if rev.state != 'deleted' %} - + {% endif %} {% if rev.state == 'deleted' %} - + {% endif %}
  • From 36215a3e4914727cc2650a6bf7a2bb44e286d1b2 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:53 +0100 Subject: [PATCH 125/189] [#858] Whitespace cleanup ckan/templates/revision/snippets/revisions_list.html --- ckan/templates/revision/snippets/revisions_list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/templates/revision/snippets/revisions_list.html b/ckan/templates/revision/snippets/revisions_list.html index 2726df8dab7..d63891581d6 100644 --- a/ckan/templates/revision/snippets/revisions_list.html +++ b/ckan/templates/revision/snippets/revisions_list.html @@ -28,6 +28,6 @@ {{ rev.message }} - {% endfor %} + {% endfor %} From 161e4573ca7d8ee0f72ffec15a3ce957a477d6ca Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:53 +0100 Subject: [PATCH 126/189] [#858] Whitespace cleanup ckan/templates/snippets/activity_item.html --- ckan/templates/snippets/activity_item.html | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/ckan/templates/snippets/activity_item.html b/ckan/templates/snippets/activity_item.html index 1923dee083c..ef86e368212 100644 --- a/ckan/templates/snippets/activity_item.html +++ b/ckan/templates/snippets/activity_item.html @@ -1,10 +1,10 @@
  • - {% if activity.is_new %} - {{ _('New activity item') }} - {% endif %} - -

    - {{ h.literal(activity.msg.format(**activity.data)) }} - {{ h.time_ago_in_words_from_str(activity.timestamp, 'hour') }} ago -

    -
  • \ No newline at end of file + {% if activity.is_new %} + {{ _('New activity item') }} + {% endif %} + +

    + {{ h.literal(activity.msg.format(**activity.data)) }} + {{ h.time_ago_in_words_from_str(activity.timestamp, 'hour') }} ago +

    + From 766a75024a90151ce8291d5bdf5d31c5630ce355 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:53 +0100 Subject: [PATCH 127/189] [#858] Whitespace cleanup ckan/templates/snippets/context/dataset.html --- ckan/templates/snippets/context/dataset.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/templates/snippets/context/dataset.html b/ckan/templates/snippets/context/dataset.html index 8853bb6f711..4a62861886a 100644 --- a/ckan/templates/snippets/context/dataset.html +++ b/ckan/templates/snippets/context/dataset.html @@ -18,4 +18,4 @@
    {{ h.SI_number_span(num_tags) }}

    -
    \ No newline at end of file +
    From e3f9585f41f9251100a7185f148c88ca49cf7375 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:53 +0100 Subject: [PATCH 128/189] [#858] Whitespace cleanup ckan/templates/snippets/context/group.html --- ckan/templates/snippets/context/group.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/templates/snippets/context/group.html b/ckan/templates/snippets/context/group.html index 6a97c6c93a7..d78f1b96785 100644 --- a/ckan/templates/snippets/context/group.html +++ b/ckan/templates/snippets/context/group.html @@ -18,4 +18,4 @@
    {{ h.SI_number_span(package_count) }}
    -

    \ No newline at end of file +
    From 5a2067732f54ac8208c2457b4b1e81878303d56d Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:53 +0100 Subject: [PATCH 129/189] [#858] Whitespace cleanup ckan/templates/snippets/context/user.html --- ckan/templates/snippets/context/user.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/templates/snippets/context/user.html b/ckan/templates/snippets/context/user.html index d322298c322..e52d6196ff6 100644 --- a/ckan/templates/snippets/context/user.html +++ b/ckan/templates/snippets/context/user.html @@ -24,4 +24,4 @@
    {{ h.SI_number_span(number_of_edits) }}
    -
    \ No newline at end of file +
    From 9b390883426984c66d201d028329e0b9c1b0407b Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:53 +0100 Subject: [PATCH 130/189] [#858] Whitespace cleanup ckan/templates/snippets/disqus_trackback.html --- ckan/templates/snippets/disqus_trackback.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/templates/snippets/disqus_trackback.html b/ckan/templates/snippets/disqus_trackback.html index b5aa7ed77df..058138d6fd3 100644 --- a/ckan/templates/snippets/disqus_trackback.html +++ b/ckan/templates/snippets/disqus_trackback.html @@ -1,4 +1,4 @@
    -

    {{ _('Trackback URL') }}

    +

    {{ _('Trackback URL') }}

    From 2da8bf6091988d89ffdcd2d17148cadd93b266a7 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:53 +0100 Subject: [PATCH 131/189] [#858] Whitespace cleanup ckan/templates/snippets/follow_button.html --- ckan/templates/snippets/follow_button.html | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/ckan/templates/snippets/follow_button.html b/ckan/templates/snippets/follow_button.html index 295aaac4c02..21f29aed9df 100644 --- a/ckan/templates/snippets/follow_button.html +++ b/ckan/templates/snippets/follow_button.html @@ -1,16 +1,16 @@ {% set controller = obj_type %} {% if controller == 'dataset' %} - {% set controller = 'package' %} + {% set controller = 'package' %} {% endif %} {% if following %} - - - {{ _('Unfollow') }} - + + + {{ _('Unfollow') }} + {% else %} - - - {{ _('Follow') }} - + + + {{ _('Follow') }} + {% endif %} From 5c75219213964289b597659d92a1cad873edcb94 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:53 +0100 Subject: [PATCH 132/189] [#858] Whitespace cleanup ckan/templates/snippets/license.html --- ckan/templates/snippets/license.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/templates/snippets/license.html b/ckan/templates/snippets/license.html index d575ef0515b..78c302a7f79 100644 --- a/ckan/templates/snippets/license.html +++ b/ckan/templates/snippets/license.html @@ -10,7 +10,7 @@

    {{ _('License') {{ pkg_dict.license_title }} {% endif %} {% if not text_only %} - {% if pkg_dict.isopen %} + {% if pkg_dict.isopen %} [Open Data] From 02d5183b4789e9b0780d37fcbb1c7da647530d31 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:53 +0100 Subject: [PATCH 133/189] [#858] Whitespace cleanup ckan/templates/snippets/private.html --- ckan/templates/snippets/private.html | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ckan/templates/snippets/private.html b/ckan/templates/snippets/private.html index 060dd4faafc..70c86ae2e79 100644 --- a/ckan/templates/snippets/private.html +++ b/ckan/templates/snippets/private.html @@ -1,4 +1,3 @@ - -
    +

    {{ _('Private') }}

    -
    +
    From ba5651b7d95a4867af71b4e59db700d439e10fa8 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:53 +0100 Subject: [PATCH 134/189] [#858] Whitespace cleanup ckan/templates/tag/index.html --- ckan/templates/tag/index.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ckan/templates/tag/index.html b/ckan/templates/tag/index.html index d91a0eadb46..5f65c580517 100644 --- a/ckan/templates/tag/index.html +++ b/ckan/templates/tag/index.html @@ -11,17 +11,17 @@

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

    {% block tags_list %} -
      - {% block tags_list_inner %} - {% for tag in c.page.items %} -
    • {{ h.link_to(tag.display_name, h.url_for(controller='package', action='search', tags=tag.name), class_='tag') }}
    • - {% endfor %} - {% endblock %} -
    +
      + {% block tags_list_inner %} + {% for tag in c.page.items %} +
    • {{ h.link_to(tag.display_name, h.url_for(controller='package', action='search', tags=tag.name), class_='tag') }}
    • + {% endfor %} + {% endblock %} +
    {% endblock %}
    {% block page_pagination %} - {{ c.page.pager(q=c.q) }} + {{ c.page.pager(q=c.q) }} {% endblock %} {% endblock %} From df80ac593e26d078211d9651ad249a1ccd311468 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:53 +0100 Subject: [PATCH 135/189] [#858] Whitespace cleanup ckan/templates/user/activity_stream.html --- ckan/templates/user/activity_stream.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/templates/user/activity_stream.html b/ckan/templates/user/activity_stream.html index 2d85e26c96a..092e6ea8485 100644 --- a/ckan/templates/user/activity_stream.html +++ b/ckan/templates/user/activity_stream.html @@ -5,6 +5,6 @@ {% block primary_content_inner %}

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

    {% block activity_stream %} - {{ c.user_activity_stream | safe }} + {{ c.user_activity_stream | safe }} {% endblock %} {% endblock %} From 55646fad517c5ac337426198382194f4c689f594 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:53 +0100 Subject: [PATCH 136/189] [#858] Whitespace cleanup ckan/templates/user/dashboard.html --- ckan/templates/user/dashboard.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ckan/templates/user/dashboard.html b/ckan/templates/user/dashboard.html index f27dd66193e..3109a632036 100644 --- a/ckan/templates/user/dashboard.html +++ b/ckan/templates/user/dashboard.html @@ -32,12 +32,12 @@ {% snippet 'user/snippets/followee_dropdown.html', context=c.dashboard_activity_stream_context, followees=c.followee_list %}

    {% block page_heading %} - {{ _('News feed') }} + {{ _('News feed') }} {% endblock %} {{ _("Activity from items that I'm following") }}

    {% block activity_stream %} - {{ c.dashboard_activity_stream }} + {{ c.dashboard_activity_stream }} {% endblock %}

    {% endblock %} From f11183f81c89e25db3045678c5ac3f677d2ab2f3 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:54 +0100 Subject: [PATCH 137/189] [#858] Whitespace cleanup ckan/templates/user/edit.html --- ckan/templates/user/edit.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/templates/user/edit.html b/ckan/templates/user/edit.html index 7246dbc4e49..3845684f78d 100644 --- a/ckan/templates/user/edit.html +++ b/ckan/templates/user/edit.html @@ -14,7 +14,7 @@ {% endblock %} {% block primary_content_inner %} - {{ c.form | safe }} + {{ c.form | safe }} {% endblock %} {% block secondary_content %} From ea50bcff3e2f8c48501907db1d53a3215dfd4b6e Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:54 +0100 Subject: [PATCH 138/189] [#858] Whitespace cleanup ckan/templates/user/followers.html --- ckan/templates/user/followers.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/templates/user/followers.html b/ckan/templates/user/followers.html index 43264762c39..3a43d3c6f8d 100644 --- a/ckan/templates/user/followers.html +++ b/ckan/templates/user/followers.html @@ -7,6 +7,6 @@

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

    {% block followers_list %} - {% snippet "user/snippets/followers.html", followers=c.followers %} + {% snippet "user/snippets/followers.html", followers=c.followers %} {% endblock %} {% endblock %} From 03781ef72fc673181705997c09fc8a32e6336414 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:54 +0100 Subject: [PATCH 139/189] [#858] Whitespace cleanup ckan/templates/user/list.html --- ckan/templates/user/list.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ckan/templates/user/list.html b/ckan/templates/user/list.html index a1fd9d7195c..0b802846737 100644 --- a/ckan/templates/user/list.html +++ b/ckan/templates/user/list.html @@ -13,17 +13,17 @@

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

    {% block users_list %} -
      - {% block users_list_inner %} - {% for user in c.page.items %} -
    • {{ h.linked_user(user['name'], maxlength=20) }}
    • - {% endfor %} - {% endblock %} -
    +
      + {% block users_list_inner %} + {% for user in c.page.items %} +
    • {{ h.linked_user(user['name'], maxlength=20) }}
    • + {% endfor %} + {% endblock %} +
    {% endblock %} {% block page_pagination %} - {{ c.page.pager(q=c.q, order_by=c.order_by) }} + {{ c.page.pager(q=c.q, order_by=c.order_by) }} {% endblock %} {% endblock %} From a8ab0365e69b0109f4ea569e604bf8a38fdb1c50 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:54 +0100 Subject: [PATCH 140/189] [#858] Whitespace cleanup ckan/templates/user/logout_first.html --- ckan/templates/user/logout_first.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/templates/user/logout_first.html b/ckan/templates/user/logout_first.html index c97eb42a9fb..acf9dddea2d 100644 --- a/ckan/templates/user/logout_first.html +++ b/ckan/templates/user/logout_first.html @@ -8,7 +8,7 @@ {% block form %}
    {{ _("You're already logged in as {user}.").format(user=c.user) }} {{ _('Logout') }}?
    - {{ form.input('login', label=_('Username'), id='field-login', value="", error="", classes=["control-full"], attrs={"disabled": "disabled"}) }} + {{ form.input('login', label=_('Username'), id='field-login', value="", error="", classes=["control-full"], attrs={"disabled": "disabled"}) }} {{ form.input('password', label=_('Password'), id='field-password', type="password", value="", error="", classes=["control-full"], attrs={"disabled": "disabled"}) }} {{ form.checkbox('remember', label=_('Remember me'), id='field-remember', checked=true, attrs={"disabled": "disabled"}) }}
    From 44ae4a503658fd7fc1663aeb093a45bf8bea6b47 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:54 +0100 Subject: [PATCH 141/189] [#858] Whitespace cleanup ckan/templates/user/read_base.html --- ckan/templates/user/read_base.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ckan/templates/user/read_base.html b/ckan/templates/user/read_base.html index aa27a2b4119..c0747e2fb9c 100644 --- a/ckan/templates/user/read_base.html +++ b/ckan/templates/user/read_base.html @@ -67,20 +67,20 @@

    {{ user.display_name }}

    {% endif %} {% if c.is_myself %} -
    -
    {{ _('Email') }} {{ _('Private') }}
    -
    {{ user.email }}
    -
    +
    +
    {{ _('Email') }} {{ _('Private') }}
    +
    {{ user.email }}
    +
    {% endif %}
    {{ _('Member Since') }}
    {{ h.render_datetime(user.created) }}
    {% if c.is_myself %} -
    -
    {{ _('API Key') }} {{ _('Private') }}
    -
    {{ user.apikey }}
    -
    +
    +
    {{ _('API Key') }} {{ _('Private') }}
    +
    {{ user.apikey }}
    +
    {% endif %}
    {% endblock %} From 1c7880d8eaef35528466c6061e57e0f837d323c0 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:54 +0100 Subject: [PATCH 142/189] [#858] Whitespace cleanup ckan/templates/user/snippets/followee_dropdown.html --- ckan/templates/user/snippets/followee_dropdown.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ckan/templates/user/snippets/followee_dropdown.html b/ckan/templates/user/snippets/followee_dropdown.html index f76e5fb9ebf..33f19d6efc5 100644 --- a/ckan/templates/user/snippets/followee_dropdown.html +++ b/ckan/templates/user/snippets/followee_dropdown.html @@ -30,12 +30,12 @@ {% for followee in followees %} - - - {{followee_icon(followee.type)}} - {{followee.display_name}} - - + + + {{followee_icon(followee.type)}} + {{followee.display_name}} + + {% endfor %} {% else %} From 293ddf6eda9573096554e9992ac0a292281cc086 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 6 Aug 2013 17:51:54 +0100 Subject: [PATCH 143/189] [#858] Whitespace cleanup ckan/templates/user/snippets/followers.html --- ckan/templates/user/snippets/followers.html | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ckan/templates/user/snippets/followers.html b/ckan/templates/user/snippets/followers.html index a587fd42cde..742faec0917 100644 --- a/ckan/templates/user/snippets/followers.html +++ b/ckan/templates/user/snippets/followers.html @@ -1,10 +1,10 @@ {% if followers %} -
      - {% for follower in followers %} - {# HACK: (for group/admins) follower can be the id of the user or a user_dict #} -
    • {{ h.linked_user(follower.name or follower, maxlength=20) }}
    • - {% endfor %} -
    +
      + {% for follower in followers %} + {# HACK: (for group/admins) follower can be the id of the user or a user_dict #} +
    • {{ h.linked_user(follower.name or follower, maxlength=20) }}
    • + {% endfor %} +
    {% else %} -

    {{ _('No followers') }}

    +

    {{ _('No followers') }}

    {% endif %} From 669aefb71e7d4c5b72366c5dbae60d4f45bf313a Mon Sep 17 00:00:00 2001 From: joetsoi Date: Sun, 11 Aug 2013 17:55:07 +0100 Subject: [PATCH 144/189] [#1117] use model.repo.delete_all() in test helpers --- ckan/new_tests/helpers.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ckan/new_tests/helpers.py b/ckan/new_tests/helpers.py index 2b35418bde0..371f5e02787 100644 --- a/ckan/new_tests/helpers.py +++ b/ckan/new_tests/helpers.py @@ -40,9 +40,7 @@ def reset_db(): # Clean out any data from the db. This prevents tests failing due to data # leftover from other tests or from previous test runs. - for table in reversed(model.meta.metadata.sorted_tables): - model.meta.Session.execute(table.delete()) - model.meta.Session.commit() + model.repo.delete_all() def call_action(action_name, context=None, **kwargs): From 6b1b6f082a402a43815565a82ba407caeb07edcd Mon Sep 17 00:00:00 2001 From: John Martin Date: Thu, 29 Aug 2013 16:05:50 +0100 Subject: [PATCH 145/189] [#1126] Brings featured groups/orgs from from be06121 into this branch It's just a slightly edited version of https://github.com/okfn/ckan/blob/be06121/ckanext/featured/plugin.py as now the helpers are returning data and not data+html. --- ckanext/homepage/plugin.py | 67 +++++++++++++++++-- .../home/snippets/featured_group.html | 10 +-- .../home/snippets/featured_organization.html | 10 +-- 3 files changed, 73 insertions(+), 14 deletions(-) diff --git a/ckanext/homepage/plugin.py b/ckanext/homepage/plugin.py index ef429f0457d..e59541a16d1 100644 --- a/ckanext/homepage/plugin.py +++ b/ckanext/homepage/plugin.py @@ -6,19 +6,74 @@ class HomepagePlugin(p.SingletonPlugin): p.implements(p.IConfigurer, inherit=True) + p.implements(p.ITemplateHelpers, inherit=True) p.implements(p.IConfigurable, inherit=True) + featured_groups_cache = None + featured_orgs_cache = None + + def configure(self, config): + groups = config.get('ckan.featured_groups', '') + if groups: + log.warning('Config setting `ckan.featured_groups` is deprecated ' + 'please use `ckanext.homepage.groups`') + self.groups = config.get('ckanext.homepage.groups', groups).split() + self.orgs = config.get('ckanext.homepage.orgs', '').split() + def update_config(self, config): p.toolkit.add_template_directory(config, 'theme/templates') - def get_featured_organization(self): - return + def get_featured_organizations(self, count=1): + orgs = self.featured_group_org(get_action='organization_show', + list_action='organization_list', + count=count, + items=self.orgs) + self.featured_orgs_cache = orgs + return self.featured_orgs_cache + + def get_featured_groups(self, count=1): + groups = self.featured_group_org(get_action='group_show', + list_action='group_list', + count=count, + items=self.groups) + self.featured_groups_cache = groups + return self.featured_groups_cache + + def featured_group_org(self, items, get_action, list_action, count): + def get_group(id): + context = {'ignore_auth': True, + 'limits': {'packages': 2}, + 'for_view': True} + data_dict = {'id': id} + + try: + out = p.toolkit.get_action(get_action)(context, data_dict) + except p.toolkit.ObjectNotFound: + return None + return out + + groups_data = [] + + extras = p.toolkit.get_action(list_action)({}, {}) + + # list of found ids to prevent duplicates + found = [] + for group_name in items + extras: + group = get_group(group_name) + if not group: + continue + # ckeck if duplicate + if group['id'] in found: + continue + found.append(group['id']) + groups_data.append(group) + if len(groups_data) == count: + break - def get_featured_group(self): - return + return groups_data def get_helpers(self): return { - 'get_featured_organization': self.get_featured_organization, - 'get_featured_group': self.get_featured_group, + 'get_featured_organizations': self.get_featured_organizations, + 'get_featured_groups': self.get_featured_groups, } diff --git a/ckanext/homepage/theme/templates/home/snippets/featured_group.html b/ckanext/homepage/theme/templates/home/snippets/featured_group.html index fb5ff22a640..d4069faf618 100644 --- a/ckanext/homepage/theme/templates/home/snippets/featured_group.html +++ b/ckanext/homepage/theme/templates/home/snippets/featured_group.html @@ -1,5 +1,7 @@ -{% set group = h.get_featured_group() %} +{% set groups = h.get_featured_groups() %} -
    - {% snippet 'snippets/group_item.html', group=group, truncate=50, truncate_title=35 %} -
    +{% for group in groups %} +
    + {% snippet 'snippets/group_item.html', group=group, truncate=50, truncate_title=35 %} +
    +{% endfor %} diff --git a/ckanext/homepage/theme/templates/home/snippets/featured_organization.html b/ckanext/homepage/theme/templates/home/snippets/featured_organization.html index 7e43e6c4bcb..7cf9ddb7482 100644 --- a/ckanext/homepage/theme/templates/home/snippets/featured_organization.html +++ b/ckanext/homepage/theme/templates/home/snippets/featured_organization.html @@ -1,5 +1,7 @@ -{% set organization = h.get_featured_organization() %} +{% set organizations = h.get_featured_organizations() %} -
    - {% snippet 'snippets/organization_item.html', organization=organization, truncate=50, truncate_title=35 %} -
    +{% for organization in organizations %} +
    + {% snippet 'snippets/organization_item.html', organization=organization, truncate=50, truncate_title=35 %} +
    +{% endfor %} From 597a6db5dbb971a1f236db841fa010551831dcd8 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Mon, 2 Sep 2013 12:57:12 +0200 Subject: [PATCH 146/189] [#744] Fix package and resource view tracking An optimization broke the package and resource view tracking. Package and resource dicts are now sometimes gotten from the Solr cache instead of dictized each time, but these cached dicts may contain outdated tracking data. Add the tracking data to the package and resource dicts in the logic instead of in the dictization. So the tracking data is now added after the dictization or Solr cache retrieval, this way the tracking data is always the latest. --- ckan/lib/dictization/model_dictize.py | 8 -------- ckan/logic/action/get.py | 25 +++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/ckan/lib/dictization/model_dictize.py b/ckan/lib/dictization/model_dictize.py index 793f0733c95..7796eccb9a0 100644 --- a/ckan/lib/dictization/model_dictize.py +++ b/ckan/lib/dictization/model_dictize.py @@ -132,11 +132,6 @@ def resource_dictize(res, context): extras = resource.pop("extras", None) if extras: resource.update(extras) - #tracking - if not context.get('for_edit'): - model = context['model'] - tracking = model.TrackingSummary.get_for_resource(res.url) - resource['tracking_summary'] = tracking resource['format'] = _unified_resource_format(res.format) # some urls do not have the protocol this adds http:// to these url = resource['url'] @@ -241,9 +236,6 @@ def package_dictize(pkg, context): q = select([extra_rev]).where(extra_rev.c.package_id == pkg.id) result = _execute_with_revision(q, extra_rev, context) result_dict["extras"] = extras_list_dictize(result, context) - #tracking - tracking = model.TrackingSummary.get_for_package(pkg.id) - result_dict['tracking_summary'] = tracking #groups member_rev = model.member_revision_table group = model.group_table diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index 7c9cdfddace..29347c5daf6 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -797,6 +797,20 @@ def package_show(context, data_dict): package_dict = model_dictize.package_dictize(pkg, context) package_dict_validated = False + # Add page-view tracking summary data to the package dict. + # If the package_dict came from the Solr cache then it will already have a + # potentially outdated tracking_summary, this will overwrite it with a + # current one. + package_dict['tracking_summary'] = model.TrackingSummary.get_for_package( + package_dict['id']) + + # Add page-view tracking summary data to the package's resource dicts. + # If the package_dict came from the Solr cache then each resource dict will + # already have a potentially outdated tracking_summary, this will overwrite + # it with a current one. + for resource_dict in package_dict['resources']: + _add_tracking_summary_to_resource_dict(resource_dict, model) + if context.get('for_view'): for item in plugins.PluginImplementations(plugins.IPackageController): package_dict = item.before_view(package_dict) @@ -824,6 +838,15 @@ def package_show(context, data_dict): return package_dict +def _add_tracking_summary_to_resource_dict(resource_dict, model): + '''Add page-view tracking summary data to the given resource dict. + + ''' + tracking_summary = model.TrackingSummary.get_for_resource( + resource_dict['url']) + resource_dict['tracking_summary'] = tracking_summary + + def resource_show(context, data_dict): '''Return the metadata of a resource. @@ -845,6 +868,8 @@ def resource_show(context, data_dict): _check_access('resource_show', context, data_dict) resource_dict = model_dictize.resource_dictize(resource, context) + _add_tracking_summary_to_resource_dict(resource_dict, model) + for item in plugins.PluginImplementations(plugins.IResourceController): resource_dict = item.before_show(resource_dict) From 9d449fb16896a266dbc92eabf85291884ff71749 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Mon, 2 Sep 2013 14:16:45 +0200 Subject: [PATCH 147/189] [#744] Add some TODOs to page view tracking tests --- ckan/tests/functional/test_tracking.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ckan/tests/functional/test_tracking.py b/ckan/tests/functional/test_tracking.py index 4f560771914..eba37ab3350 100644 --- a/ckan/tests/functional/test_tracking.py +++ b/ckan/tests/functional/test_tracking.py @@ -492,6 +492,7 @@ def test_sorting_datasets_by_total_views(self): assert packages[2]['name'] == 'consider_phlebas' def test_popular_package(self): + # TODO # Test that a package with > 10 views is marked as 'popular'. # Currently the popular logic is in the templates, will have to move # that into the logic and add 'popular': True/False to package dicts @@ -502,6 +503,7 @@ def test_popular_package(self): pass def test_popular_resource(self): + # TODO # Test that a resource with > 10 views is marked as 'popular'. # Currently the popular logic is in the templates, will have to move # that into the logic and add 'popular': True/False to resource dicts @@ -512,12 +514,14 @@ def test_popular_resource(self): pass def test_same_user_visiting_different_pages_on_same_day(self): + # TODO # Test that if the same user visits multiple pages on the same say, # each visit gets counted (this should not get throttled) # (May need to test for packages, resources and pages separately) pass def test_same_user_visiting_same_page_on_different_days(self): + # TODO # Test that if the same user visits the same page on different days, # each visit gets counted (this should not get throttled) # (May need to test for packages, resources and pages separately) @@ -526,7 +530,7 @@ def test_same_user_visiting_same_page_on_different_days(self): pass def test_posting_bad_data_to_tracking(self): - # Test how /_tracking handles unexpected and invalid data. + # TODO: Test how /_tracking handles unexpected and invalid data. pass def _export_tracking_summary(self): @@ -553,6 +557,7 @@ def test_export(self): pass def test_tracking_urls_with_languages(self): + # TODO # Test that posting to eg /de/dataset/foo is counted the same as # /dataset/foo. # May need to test for dataset pages, resource previews, resource @@ -560,18 +565,21 @@ def test_tracking_urls_with_languages(self): pass def test_templates_tracking_enabled(self): + # TODO # Test the the page view tracking JS is in the templates when # ckan.tracking_enabled = true. # Test that the sort by populatiy option is shown on the datasets page. pass def test_templates_tracking_disabled(self): + # TODO # Test the the page view tracking JS is not in the templates when # ckan.tracking_enabled = false. # Test that the sort by populatiy option is not on the datasets page. pass def test_tracking_disabled(self): + # TODO # Just to make sure, set ckan.tracking_enabled = false and then post # a bunch of stuff to /_tracking and test that no tracking data is # recorded. Maybe /_tracking should return something other than 200, From db4fb6ee06714f67200348c8f34a1905aafa7989 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Mon, 2 Sep 2013 14:17:21 +0200 Subject: [PATCH 148/189] [#714] Add a page view tracking test --- ckan/tests/functional/test_tracking.py | 56 ++++++++++++++++++-------- 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/ckan/tests/functional/test_tracking.py b/ckan/tests/functional/test_tracking.py index eba37ab3350..5c3090fee45 100644 --- a/ckan/tests/functional/test_tracking.py +++ b/ckan/tests/functional/test_tracking.py @@ -3,6 +3,7 @@ import tempfile import csv import datetime +import routes import ckan.tests as tests @@ -138,8 +139,6 @@ def test_resource_with_0_views(self): "been viewed should have 0 total views") def test_package_with_one_view(self): - import routes - app = self._get_app() sysadmin_user, apikey = self._create_sysadmin(app) package = self._create_package(app, apikey) @@ -167,8 +166,6 @@ def test_package_with_one_view(self): "not increase the total views of the package's resources") def test_resource_with_one_preview(self): - import routes - app = self._get_app() sysadmin_user, apikey = self._create_sysadmin(app) package = self._create_package(app, apikey) @@ -254,8 +251,6 @@ def test_view_page(self): "page is always 0") def test_package_with_many_views(self): - import routes - app = self._get_app() sysadmin_user, apikey = self._create_sysadmin(app) package = self._create_package(app, apikey) @@ -361,8 +356,6 @@ def test_dataset_view_count_throttling(self): day, only one view should get counted. ''' - import routes - app = self._get_app() sysadmin_user, apikey = self._create_sysadmin(app) package = self._create_package(app, apikey) @@ -412,7 +405,6 @@ def test_resource_download_count_throttling(self): def test_sorting_datasets_by_recent_views(self): # FIXME: Have some datasets with different numbers of recent and total # views, to make this a better test. - import routes import ckan.lib.search tests.setup_test_search_index() @@ -453,7 +445,6 @@ def test_sorting_datasets_by_recent_views(self): def test_sorting_datasets_by_total_views(self): # FIXME: Have some datasets with different numbers of recent and total # views, to make this a better test. - import routes import ckan.lib.search tests.setup_test_search_index() @@ -546,15 +537,48 @@ def _export_tracking_summary(self): import ckan.model f = tempfile.NamedTemporaryFile() ckan.lib.cli.Tracking('Tracking').export_tracking( - engine=ckan.model.meta.engine, output_filename=f.name) - lines = csv.DictReader(open(f.name, 'r')) + engine=ckan.model.meta.engine, output_filename=f.name) + lines = [line for line in csv.DictReader(open(f.name, 'r'))] return lines def test_export(self): - # Create some packages and resources, visit them and some other pages, - # export all the tracking data to CSV and check that the exported - # values are correct. - pass + '''`paster tracking export` should export tracking data for all + datasets in CSV format. + + Only dataset tracking data is output to CSV file, not resource or page + views. + + ''' + app = self._get_app() + sysadmin_user, apikey = self._create_sysadmin(app) + + # Create a couple of packages. + package_1 = self._create_package(app, apikey) + package_2 = self._create_package(app, apikey, name='another_package') + + # View the package_1 three times from different IPs. + url = routes.url_for(controller='package', action='read', + id=package_1['name']) + self._post_to_tracking(app, url, ip='111.222.333.44') + self._post_to_tracking(app, url, ip='111.222.333.55') + self._post_to_tracking(app, url, ip='111.222.333.66') + + # View the package_2 twice from different IPs. + url = routes.url_for(controller='package', action='read', + id=package_2['name']) + self._post_to_tracking(app, url, ip='111.222.333.44') + self._post_to_tracking(app, url, ip='111.222.333.55') + + self._update_tracking_summary() + lines = self._export_tracking_summary() + + assert len(lines) == 2 + package_1_data = lines[0] + assert package_1_data['total views'] == '3' + assert package_1_data['recent views (last 2 weeks)'] == '3' + package_2_data = lines[1] + assert package_2_data['total views'] == '2' + assert package_2_data['recent views (last 2 weeks)'] == '2' def test_tracking_urls_with_languages(self): # TODO From 7382643f5ac63fef272a4cdda66f98cceeed43ea Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Mon, 2 Sep 2013 14:33:28 +0200 Subject: [PATCH 149/189] [#744] PEP-8 --- ckan/tests/functional/test_tracking.py | 216 +++++++++++++++---------- 1 file changed, 128 insertions(+), 88 deletions(-) diff --git a/ckan/tests/functional/test_tracking.py b/ckan/tests/functional/test_tracking.py index 5c3090fee45..44393036f5f 100644 --- a/ckan/tests/functional/test_tracking.py +++ b/ckan/tests/functional/test_tracking.py @@ -32,7 +32,7 @@ def _create_sysadmin(self, app): # user. import ckan.model as model user = model.User(name='joeadmin', email='joe@admin.net', - password='joe rules') + password='joe rules') user.sysadmin = True model.Session.add(user) model.repo.commit_and_remove() @@ -43,16 +43,17 @@ def _create_package(self, app, apikey, name='look_to_windward'): '''Create a package via the action api.''' return tests.call_action_api(app, 'package_create', apikey=apikey, - name=name) + name=name) def _create_resource(self, app, package, apikey): '''Create a resource via the action api.''' return tests.call_action_api(app, 'resource_create', apikey=apikey, - package_id=package['id'], url='http://example.com') + package_id=package['id'], + url='http://example.com') def _post_to_tracking(self, app, url, type_='page', ip='199.204.138.90', - browser='firefox'): + browser='firefox'): '''Post some data to /_tracking directly. This simulates what's supposed when you view a page with tracking @@ -61,13 +62,13 @@ def _post_to_tracking(self, app, url, type_='page', ip='199.204.138.90', ''' params = {'url': url, 'type': type_} app.post('/_tracking', params=params, - extra_environ={ - # The tracking middleware crashes if these aren't present. - 'HTTP_USER_AGENT': browser, - 'REMOTE_ADDR': ip, - 'HTTP_ACCEPT_LANGUAGE': 'en', - 'HTTP_ACCEPT_ENCODING': 'gzip, deflate', - }) + extra_environ={ + # The tracking middleware crashes if these aren't present. + 'HTTP_USER_AGENT': browser, + 'REMOTE_ADDR': ip, + 'HTTP_ACCEPT_LANGUAGE': 'en', + 'HTTP_ACCEPT_ENCODING': 'gzip, deflate', + }) def _update_tracking_summary(self): '''Update CKAN's tracking summary data. @@ -81,9 +82,9 @@ def _update_tracking_summary(self): import ckan.lib.cli import ckan.model date = (datetime.datetime.now() - datetime.timedelta(days=1)).strftime( - '%Y-%m-%d') + '%Y-%m-%d') ckan.lib.cli.Tracking('Tracking').update_all( - engine=ckan.model.meta.engine, start_date=date) + engine=ckan.model.meta.engine, start_date=date) def _rebuild_search_index(self): '''Rebuild CKAN's search index. @@ -103,12 +104,14 @@ def test_package_with_0_views(self): # The API should return 0 recent views and 0 total views for the # unviewed package. package = tests.call_action_api(app, 'package_show', - id=package['name']) + id=package['name']) tracking_summary = package['tracking_summary'] assert tracking_summary['recent'] == 0, ("A package that has not " - "been viewed should have 0 recent views") + "been viewed should have 0 " + "recent views") assert tracking_summary['total'] == 0, ("A package that has not " - "been viewed should have 0 total views") + "been viewed should have 0 " + "total views") def test_resource_with_0_views(self): app = self._get_app() @@ -119,24 +122,28 @@ def test_resource_with_0_views(self): # The package_show() API should return 0 recent views and 0 total # views for the unviewed resource. package = tests.call_action_api(app, 'package_show', - id=package['name']) + id=package['name']) assert len(package['resources']) == 1 resource = package['resources'][0] tracking_summary = resource['tracking_summary'] assert tracking_summary['recent'] == 0, ("A resource that has not " - "been viewed should have 0 recent views") + "been viewed should have 0 " + "recent views") assert tracking_summary['total'] == 0, ("A resource that has not " - "been viewed should have 0 total views") + "been viewed should have 0 " + "total views") # The resource_show() API should return 0 recent views and 0 total # views for the unviewed resource. resource = tests.call_action_api(app, 'resource_show', - id=resource['id']) + id=resource['id']) tracking_summary = resource['tracking_summary'] assert tracking_summary['recent'] == 0, ("A resource that has not " - "been viewed should have 0 recent views") + "been viewed should have 0 " + "recent views") assert tracking_summary['total'] == 0, ("A resource that has not " - "been viewed should have 0 total views") + "been viewed should have 0 " + "total views") def test_package_with_one_view(self): app = self._get_app() @@ -145,7 +152,7 @@ def test_package_with_one_view(self): self._create_resource(app, package, apikey) url = routes.url_for(controller='package', action='read', - id=package['name']) + id=package['name']) self._post_to_tracking(app, url) self._update_tracking_summary() @@ -153,17 +160,22 @@ def test_package_with_one_view(self): package = tests.call_action_api(app, 'package_show', id=package['id']) tracking_summary = package['tracking_summary'] assert tracking_summary['recent'] == 1, ("A package that has been " - "viewed once should have 1 recent view.") + "viewed once should have 1 " + "recent view.") assert tracking_summary['total'] == 1, ("A package that has been " - "viewed once should have 1 total view") + "viewed once should have 1 " + "total view") assert len(package['resources']) == 1 resource = package['resources'][0] tracking_summary = resource['tracking_summary'] assert tracking_summary['recent'] == 0, ("Viewing a package should " - "not increase the recent views of the package's resources") + "not increase the recent " + "views of the package's " + "resources") assert tracking_summary['total'] == 0, ("Viewing a package should " - "not increase the total views of the package's resources") + "not increase the total views " + "of the package's resources") def test_resource_with_one_preview(self): app = self._get_app() @@ -172,7 +184,7 @@ def test_resource_with_one_preview(self): resource = self._create_resource(app, package, apikey) url = routes.url_for(controller='package', action='resource_read', - id=package['name'], resource_id=resource['id']) + id=package['name'], resource_id=resource['id']) self._post_to_tracking(app, url) self._update_tracking_summary() @@ -182,14 +194,26 @@ def test_resource_with_one_preview(self): resource = package['resources'][0] assert package['tracking_summary']['recent'] == 0, ("Previewing a " - "resource should not increase the package's recent views") + "resource should " + "not increase the " + "package's recent " + "views") assert package['tracking_summary']['total'] == 0, ("Previewing a " - "resource should not increase the package's total views") + "resource should " + "not increase the " + "package's total " + "views") # Yes, previewing a resource does _not_ increase its view count. assert resource['tracking_summary']['recent'] == 0, ("Previewing a " - "resource should not increase the resource's recent views") + "resource should " + "not increase " + "the resource's " + "recent views") assert resource['tracking_summary']['total'] == 0, ("Previewing a " - "resource should not increase the resource's recent views") + "resource should " + "not increase the " + "resource's " + "recent views") def test_resource_with_one_download(self): app = self._get_app() @@ -203,23 +227,29 @@ def test_resource_with_one_download(self): package = tests.call_action_api(app, 'package_show', id=package['id']) assert len(package['resources']) == 1 resource = package['resources'][0] - assert package['tracking_summary']['recent'] == 0, ("Downloading a " - "resource should not increase the package's recent views") - assert package['tracking_summary']['total'] == 0, ("Downloading a " - "resource should not increase the package's total views") - assert resource['tracking_summary']['recent'] == 1, ("Downloading a " - "resource should increase the resource's recent views") - assert resource['tracking_summary']['total'] == 1, ("Downloading a " - "resource should increase the resource's total views") + assert package['tracking_summary']['recent'] == 0, ( + "Downloading a resource should not increase the package's recent " + "views") + assert package['tracking_summary']['total'] == 0, ( + "Downloading a resource should not increase the package's total " + "views") + assert resource['tracking_summary']['recent'] == 1, ( + "Downloading a resource should increase the resource's recent " + "views") + assert resource['tracking_summary']['total'] == 1, ( + "Downloading a resource should increase the resource's total " + "views") # The resource_show() API should return the same result. resource = tests.call_action_api(app, 'resource_show', - id=resource['id']) + id=resource['id']) tracking_summary = resource['tracking_summary'] - assert tracking_summary['recent'] == 1, ("Downloading a " - "resource should increase the resource's recent views") - assert tracking_summary['total'] == 1, ("Downloading a " - "resource should increase the resource's total views") + assert tracking_summary['recent'] == 1, ( + "Downloading a resource should increase the resource's recent " + "views") + assert tracking_summary['total'] == 1, ( + "Downloading a resource should increase the resource's total " + "views") def test_view_page(self): app = self._get_app() @@ -242,13 +272,14 @@ def test_view_page(self): q = q.filter_by(url=url) tracking_summary = q.one() assert tracking_summary.count == 1, ("Viewing a page should " - "increase the page's view count") + "increase the page's view " + "count") # For pages (as opposed to datasets and resources) recent_views and # running_total always stay at 1. Shrug. - assert tracking_summary.recent_views == 0, ("recent_views for a " - "page is always 0") - assert tracking_summary.running_total == 0, ("running_total for a " - "page is always 0") + assert tracking_summary.recent_views == 0, ( + "recent_views for a page is always 0") + assert tracking_summary.running_total == 0, ( + "running_total for a page is always 0") def test_package_with_many_views(self): app = self._get_app() @@ -257,7 +288,7 @@ def test_package_with_many_views(self): self._create_resource(app, package, apikey) url = routes.url_for(controller='package', action='read', - id=package['name']) + id=package['name']) # View the package three times from different IPs. self._post_to_tracking(app, url, ip='111.222.333.44') @@ -268,18 +299,21 @@ def test_package_with_many_views(self): package = tests.call_action_api(app, 'package_show', id=package['id']) tracking_summary = package['tracking_summary'] - assert tracking_summary['recent'] == 3, ("A package that has been " - "viewed 3 times recently should have 3 recent views") - assert tracking_summary['total'] == 3, ("A package that has been " - "viewed 3 times should have 3 total views") + assert tracking_summary['recent'] == 3, ( + "A package that has been viewed 3 times recently should have 3 " + "recent views") + assert tracking_summary['total'] == 3, ( + "A package that has been viewed 3 times should have 3 total views") assert len(package['resources']) == 1 resource = package['resources'][0] tracking_summary = resource['tracking_summary'] - assert tracking_summary['recent'] == 0, ("Viewing a package should " - "not increase the recent views of the package's resources") - assert tracking_summary['total'] == 0, ("Viewing a package should " - "not increase the total views of the package's resources") + assert tracking_summary['recent'] == 0, ( + "Viewing a package should not increase the recent views of the " + "package's resources") + assert tracking_summary['total'] == 0, ( + "Viewing a package should not increase the total views of the " + "package's resources") def test_resource_with_many_downloads(self): app = self._get_app() @@ -299,16 +333,20 @@ def test_resource_with_many_downloads(self): assert len(package['resources']) == 1 resource = package['resources'][0] tracking_summary = resource['tracking_summary'] - assert tracking_summary['recent'] == 3, ("A resource that has been " - "downloaded 3 times recently should have 3 recent downloads") - assert tracking_summary['total'] == 3, ("A resource that has been " - "downloaded 3 times should have 3 total downloads") + assert tracking_summary['recent'] == 3, ( + "A resource that has been downloaded 3 times recently should have " + "3 recent downloads") + assert tracking_summary['total'] == 3, ( + "A resource that has been downloaded 3 times should have 3 total " + "downloads") tracking_summary = package['tracking_summary'] - assert tracking_summary['recent'] == 0, ("Downloading a resource " - "should not increase the resource's package's recent views") - assert tracking_summary['total'] == 0, ("Downloading a resource " - "should not increase the resource's package's total views") + assert tracking_summary['recent'] == 0, ( + "Downloading a resource should not increase the resource's " + "package's recent views") + assert tracking_summary['total'] == 0, ( + "Downloading a resource should not increase the resource's " + "package's total views") def test_page_with_many_views(self): app = self._get_app() @@ -319,7 +357,7 @@ def test_page_with_many_views(self): self._post_to_tracking(app, url='', type_='page', ip=ip) # Visit the /organization page. self._post_to_tracking(app, url='/organization', type_='page', - ip=ip) + ip=ip) # Visit the /about page. self._post_to_tracking(app, url='/about', type_='page', ip=ip) @@ -333,14 +371,15 @@ def test_page_with_many_views(self): q = model.Session.query(model.TrackingSummary) q = q.filter_by(url=url) tracking_summary = q.one() - assert tracking_summary.count == 3, ("A page that has been " - "viewed three times should have view count 3") + assert tracking_summary.count == 3, ( + "A page that has been viewed three times should have view " + "count 3") # For pages (as opposed to datasets and resources) recent_views and # running_total always stay at 1. Shrug. assert tracking_summary.recent_views == 0, ("recent_views for " - "pages is always 0") + "pages is always 0") assert tracking_summary.running_total == 0, ("running_total for " - "pages is always 0") + "pages is always 0") def test_recent_views_expire(self): # TODO @@ -361,7 +400,7 @@ def test_dataset_view_count_throttling(self): package = self._create_package(app, apikey) self._create_resource(app, package, apikey) url = routes.url_for(controller='package', action='read', - id=package['name']) + id=package['name']) # Visit the dataset three times from the same IP. self._post_to_tracking(app, url) @@ -373,9 +412,10 @@ def test_dataset_view_count_throttling(self): package = tests.call_action_api(app, 'package_show', id=package['id']) tracking_summary = package['tracking_summary'] assert tracking_summary['recent'] == 1, ("Repeat dataset views should " - "not add to recent views count") + "not add to recent views " + "count") assert tracking_summary['total'] == 1, ("Repeat dataset views should " - "not add to total views count") + "not add to total views count") def test_resource_download_count_throttling(self): '''If the same user downloads the same resource multiple times on the @@ -395,12 +435,12 @@ def test_resource_download_count_throttling(self): self._update_tracking_summary() resource = tests.call_action_api(app, 'resource_show', - id=resource['id']) + id=resource['id']) tracking_summary = resource['tracking_summary'] - assert tracking_summary['recent'] == 1, ("Repeat resource downloads " - "should not add to recent views count") - assert tracking_summary['total'] == 1, ("Repeat resource downloads " - "should not add to total views count") + assert tracking_summary['recent'] == 1, ( + "Repeat resource downloads should not add to recent views count") + assert tracking_summary['total'] == 1, ( + "Repeat resource downloads should not add to total views count") def test_sorting_datasets_by_recent_views(self): # FIXME: Have some datasets with different numbers of recent and total @@ -416,16 +456,16 @@ def test_sorting_datasets_by_recent_views(self): self._create_package(app, apikey, name='use_of_weapons') url = routes.url_for(controller='package', action='read', - id='consider_phlebas') + id='consider_phlebas') self._post_to_tracking(app, url) url = routes.url_for(controller='package', action='read', - id='the_player_of_games') + id='the_player_of_games') self._post_to_tracking(app, url, ip='111.11.111.111') self._post_to_tracking(app, url, ip='222.22.222.222') url = routes.url_for(controller='package', action='read', - id='use_of_weapons') + id='use_of_weapons') self._post_to_tracking(app, url, ip='111.11.111.111') self._post_to_tracking(app, url, ip='222.22.222.222') self._post_to_tracking(app, url, ip='333.33.333.333') @@ -434,7 +474,7 @@ def test_sorting_datasets_by_recent_views(self): ckan.lib.search.rebuild() response = tests.call_action_api(app, 'package_search', - sort='views_recent desc') + sort='views_recent desc') assert response['count'] == 3 assert response['sort'] == 'views_recent desc' packages = response['results'] @@ -456,16 +496,16 @@ def test_sorting_datasets_by_total_views(self): self._create_package(app, apikey, name='use_of_weapons') url = routes.url_for(controller='package', action='read', - id='consider_phlebas') + id='consider_phlebas') self._post_to_tracking(app, url) url = routes.url_for(controller='package', action='read', - id='the_player_of_games') + id='the_player_of_games') self._post_to_tracking(app, url, ip='111.11.111.111') self._post_to_tracking(app, url, ip='222.22.222.222') url = routes.url_for(controller='package', action='read', - id='use_of_weapons') + id='use_of_weapons') self._post_to_tracking(app, url, ip='111.11.111.111') self._post_to_tracking(app, url, ip='222.22.222.222') self._post_to_tracking(app, url, ip='333.33.333.333') @@ -474,7 +514,7 @@ def test_sorting_datasets_by_total_views(self): ckan.lib.search.rebuild() response = tests.call_action_api(app, 'package_search', - sort='views_total desc') + sort='views_total desc') assert response['count'] == 3 assert response['sort'] == 'views_total desc' packages = response['results'] From b5cc286d844bbd25d8fa589b919b04c3932779f8 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Mon, 2 Sep 2013 16:28:20 +0200 Subject: [PATCH 150/189] [#744] Don't add package data in Package's as_dict This is done in the logic now --- ckan/model/package.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ckan/model/package.py b/ckan/model/package.py index 619b334f52e..a6663a02e42 100644 --- a/ckan/model/package.py +++ b/ckan/model/package.py @@ -221,10 +221,6 @@ def as_dict(self, ref_package_by='name', ref_group_by='name'): import ckan.lib.helpers as h _dict['notes_rendered'] = h.render_markdown(self.notes) _dict['type'] = self.type or u'dataset' - #tracking - import ckan.model as model - tracking = model.TrackingSummary.get_for_package(self.id) - _dict['tracking_summary'] = tracking return _dict def add_relationship(self, type_, related_package, comment=u''): From 2b2e152d450615ab0d761d161a1d1f1e6ca54672 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Mon, 2 Sep 2013 16:28:57 +0200 Subject: [PATCH 151/189] [#744] Don't add tracking data in Resource's as_dict This is done in the logic now --- ckan/model/resource.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/ckan/model/resource.py b/ckan/model/resource.py index 9b2f6529895..b983e7cf4e7 100644 --- a/ckan/model/resource.py +++ b/ckan/model/resource.py @@ -15,7 +15,6 @@ import extension import activity import domain_object -import tracking as _tracking import ckan.lib.dictization __all__ = ['Resource', 'resource_table', @@ -120,8 +119,6 @@ def as_dict(self, core_columns_only=False): _dict[k] = v if self.resource_group and not core_columns_only: _dict["package_id"] = self.resource_group.package_id - tracking = _tracking.TrackingSummary.get_for_resource(self.url) - _dict['tracking_summary'] = tracking # FIXME format unification needs doing better import ckan.lib.dictization.model_dictize as model_dictize _dict[u'format'] = model_dictize._unified_resource_format(self.format) From ad0021869cb32cd10bdf11572b65e713df9cb364 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Mon, 2 Sep 2013 16:29:25 +0200 Subject: [PATCH 152/189] [#744] Update some tests Now that tracking_summary is done in logic, not dictization --- ckan/tests/lib/test_dictization.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ckan/tests/lib/test_dictization.py b/ckan/tests/lib/test_dictization.py index fb8bb49e628..01847884c8c 100644 --- a/ckan/tests/lib/test_dictization.py +++ b/ckan/tests/lib/test_dictization.py @@ -92,7 +92,6 @@ def setup_class(cls): u'size_extra': u'123', 'url_type': None, u'state': u'active', - u'tracking_summary': {'total': 0, 'recent': 0}, u'url': u'http://www.annakarenina.com/download/x=1&y=2', u'webstore_last_updated': None, u'webstore_url': None}, @@ -112,7 +111,6 @@ def setup_class(cls): u'size': None, u'size_extra': u'345', u'state': u'active', - u'tracking_summary': {'total': 0, 'recent': 0}, u'url': u'http://www.annakarenina.com/index.json', u'webstore_last_updated': None, u'webstore_url': None}], @@ -125,7 +123,6 @@ def setup_class(cls): {'name': u'tolstoy', 'display_name': u'tolstoy', 'state': u'active'}], 'title': u'A Novel By Tolstoy', - 'tracking_summary': {'total': 0, 'recent': 0}, 'url': u'http://www.annakarenina.com', 'version': u'0.7a', 'num_tags': 3, @@ -216,7 +213,6 @@ def test_01_dictize_main_objects_simple(self): 'size': None, u'size_extra': u'123', 'state': u'active', - u'tracking_summary': {'total': 0, 'recent': 0}, 'url': u'http://www.annakarenina.com/download/x=1&y=2', 'url_type': None, 'webstore_last_updated': None, @@ -751,7 +747,6 @@ def test_13_get_package_in_past(self): u'url_type': None, u'size': None, u'state': u'active', - u'tracking_summary': {'total': 0, 'recent': 0}, u'url': u'http://newurl', u'webstore_last_updated': None, u'webstore_url': None}) @@ -784,7 +779,6 @@ def test_14_resource_no_id(self): 'hash': u'abc123', 'description': u'Full text. Needs escaping: " Umlaut: \xfc', 'format': u'plain text', - 'tracking_summary': {'recent': 0, 'total': 0}, 'url': u'http://test_new', 'cache_url': None, 'webstore_url': None, From 185262a5de2d5616f0ee4495c00a6f6af3c2f0b2 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Mon, 2 Sep 2013 16:33:52 +0200 Subject: [PATCH 153/189] [#744] Update an outdated test --- ckan/tests/logic/test_action.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ckan/tests/logic/test_action.py b/ckan/tests/logic/test_action.py index 4e496f7899c..7fc45bcd315 100644 --- a/ckan/tests/logic/test_action.py +++ b/ckan/tests/logic/test_action.py @@ -877,6 +877,12 @@ def test_26_resource_show(self): postparams = '%s=1' % json.dumps({'id': resource.id}) res = self.app.post('/api/action/resource_show', params=postparams) result = json.loads(res.body)['result'] + + # Remove tracking data from the result dict. This tracking data is + # added by the logic, so the other resource dict taken straight from + # resource_dictize() won't have it. + del result['tracking_summary'] + resource_dict = resource_dictize(resource, {'model': model}) assert result == resource_dict, (result, resource_dict) From 558868108b85d43a10dd16a283cb7acefb93ce75 Mon Sep 17 00:00:00 2001 From: Nigel Babu Date: Tue, 3 Sep 2013 14:48:46 +0530 Subject: [PATCH 154/189] [#1216] Correct the logic in resource_delete resource_delete, as it stands, is broken. This fixes the logic and adds tests for resource_delete. Fixes #1216. --- ckan/logic/action/delete.py | 6 ++-- ckan/tests/logic/test_action.py | 53 +++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/ckan/logic/action/delete.py b/ckan/logic/action/delete.py index 933e7624b43..e8ba6aeb361 100644 --- a/ckan/logic/action/delete.py +++ b/ckan/logic/action/delete.py @@ -73,8 +73,10 @@ def resource_delete(context, data_dict): pkg_dict = _get_action('package_show')(context, {'id': package_id}) - if 'resources' in pkg_dict and id in pkg_dict['resources']: - pkg_dict['resources'].remove(id) + for res in pkg_dict.get('resources', []): + if res['id'] == id: + pkg_dict['resources'].remove(res) + break try: pkg_dict = _get_action('package_update')(context, pkg_dict) except ValidationError, e: diff --git a/ckan/tests/logic/test_action.py b/ckan/tests/logic/test_action.py index 4e496f7899c..607e56f1e3e 100644 --- a/ckan/tests/logic/test_action.py +++ b/ckan/tests/logic/test_action.py @@ -1705,3 +1705,56 @@ def test_2_view_group(self): res_json = json.loads(res.body) assert res_json['success'] is False + +class TestResourceAction(WsgiAppCase): + + sysadmin_user = None + + normal_user = None + + @classmethod + def setup_class(cls): + search.clear() + CreateTestData.create() + cls.sysadmin_user = model.User.get('testsysadmin') + + @classmethod + def teardown_class(cls): + model.repo.rebuild_db() + + def _add_basic_package(self, package_name=u'test_package', **kwargs): + package = { + 'name': package_name, + 'title': u'A Novel By Tolstoy', + 'resources': [{ + 'description': u'Full text.', + 'format': u'plain text', + 'url': u'http://www.annakarenina.com/download/' + }] + } + package.update(kwargs) + + postparams = '%s=1' % json.dumps(package) + res = self.app.post('/api/action/package_create', params=postparams, + extra_environ={'Authorization': 'tester'}) + return json.loads(res.body)['result'] + + def test_01_delete_resource(self): + res_dict = self._add_basic_package() + pkg_id = res_dict['id'] + + resource_count = len(res_dict['resources']) + id = res_dict['resources'][0]['id'] + url = '/api/action/resource_delete' + + # Use the sysadmin user because this package doesn't belong to an org + res = self.app.post(url, params=json.dumps({'id': id}), + extra_environ={'Authorization': str(self.sysadmin_user.apikey)}) + res_dict = json.loads(res.body) + assert res_dict['success'] is True + + url = '/api/action/package_show' + res = self.app.get(url, {'id': pkg_id}) + res_dict = json.loads(res.body) + assert res_dict['success'] is True + assert len(res_dict['result']['resources']) == resource_count - 1 From 36459a5063f32a622e44ca3aed26bb8538d5f0ed Mon Sep 17 00:00:00 2001 From: John Glover Date: Tue, 3 Sep 2013 12:53:40 +0200 Subject: [PATCH 155/189] [#1217] Fix trash redirect for instances not at / --- ckan/controllers/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/controllers/admin.py b/ckan/controllers/admin.py index cb5e84b069b..cac3a6b44b3 100644 --- a/ckan/controllers/admin.py +++ b/ckan/controllers/admin.py @@ -149,4 +149,4 @@ def trash(self): for msg in msgs: h.flash_error(msg) - h.redirect_to(h.url_for('ckanadmin', action='trash')) + h.redirect_to(controller='admin', action='trash') From 28e6ed78a854f3a4b6f171199d141a00bb0aaf06 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Wed, 4 Sep 2013 17:24:08 +0200 Subject: [PATCH 156/189] [#1222] Undeprecated extra_template_paths and extra_public_paths These config options shouldn't have been marked as deprecated. --- doc/configuration.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/doc/configuration.rst b/doc/configuration.rst index 0efe9573226..e2c230353cd 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -914,8 +914,6 @@ To customise the display of CKAN you can supply replacements for the Genshi temp For more information on theming, see :doc:`theming`. -.. note:: This is only for legacy code, and shouldn't be used anymore. - .. _extra_public_paths: extra_public_paths @@ -929,8 +927,6 @@ To customise the display of CKAN you can supply replacements for static files su For more information on theming, see :doc:`theming`. -.. note:: This is only for legacy code, and shouldn't be used anymore. - .. end_config-theming Storage Settings From 154b1c0c3057e4bcbb2ee75801ed8424bd311a9b Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 5 Sep 2013 11:48:24 +0200 Subject: [PATCH 157/189] [#744] Update an outdated test --- ckan/tests/lib/test_dictization_schema.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ckan/tests/lib/test_dictization_schema.py b/ckan/tests/lib/test_dictization_schema.py index 62bc1442ec7..5a94d2e54e8 100644 --- a/ckan/tests/lib/test_dictization_schema.py +++ b/ckan/tests/lib/test_dictization_schema.py @@ -81,14 +81,12 @@ def test_1_package_schema(self): 'format': u'plain text', 'hash': u'abc123', 'size_extra': u'123', - 'tracking_summary': {'recent': 0, 'total': 0}, 'url': u'http://www.annakarenina.com/download/x=1&y=2'}, {'alt_url': u'alt345', 'description': u'Index of the novel', 'format': u'JSON', 'hash': u'def456', 'size_extra': u'345', - 'tracking_summary': {'recent': 0, 'total': 0}, 'url': u'http://www.annakarenina.com/index.json'}], 'tags': [{'name': u'Flexible \u30a1'}, {'name': u'russian'}, From 2a84329d683029f108d243cc523e74389c2f4d2a Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 5 Sep 2013 11:51:45 +0200 Subject: [PATCH 158/189] [#744] Update an outdated test --- ckan/tests/lib/test_resource_search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/tests/lib/test_resource_search.py b/ckan/tests/lib/test_resource_search.py index a76bbdc7647..68e6fd5724d 100644 --- a/ckan/tests/lib/test_resource_search.py +++ b/ckan/tests/lib/test_resource_search.py @@ -120,7 +120,7 @@ def test_12_search_all_fields(self): assert isinstance(res_dict, dict) res_keys = set(res_dict.keys()) expected_res_keys = set(model.Resource.get_columns()) - expected_res_keys.update(['id', 'resource_group_id', 'package_id', 'position', 'size_extra', 'tracking_summary']) + expected_res_keys.update(['id', 'resource_group_id', 'package_id', 'position', 'size_extra']) assert_equal(res_keys, expected_res_keys) pkg1 = model.Package.by_name(u'pkg1') ab = pkg1.resources[0] From 67a12a7503215e0a055a75e5febf64f686733bae Mon Sep 17 00:00:00 2001 From: John Martin Date: Thu, 5 Sep 2013 15:18:02 +0100 Subject: [PATCH 159/189] [#858] Small whitespace tweaks --- ckan/templates/activity_streams/activity_stream_items.html | 4 ++-- ckan/templates/header.html | 2 +- ckan/templates/related/confirm_delete.html | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ckan/templates/activity_streams/activity_stream_items.html b/ckan/templates/activity_streams/activity_stream_items.html index 16a81d40103..9fe23bdd102 100644 --- a/ckan/templates/activity_streams/activity_stream_items.html +++ b/ckan/templates/activity_streams/activity_stream_items.html @@ -3,8 +3,8 @@ {% if activities %}
      - {% if offset > 0 %} -
    • {{ _('Load less') }}
    • + {% if offset > 0 %} +
    • {{ _('Load less') }}
    • {% endif %} {% for activity in activities %} {% if loop.index <= has_more_length %} diff --git a/ckan/templates/header.html b/ckan/templates/header.html index 336ba3a781d..58e98479aa9 100644 --- a/ckan/templates/header.html +++ b/ckan/templates/header.html @@ -32,7 +32,7 @@ - + {% endblock %} {% block header_account_log_out_link %}
    • diff --git a/ckan/templates/related/confirm_delete.html b/ckan/templates/related/confirm_delete.html index 5c9f82cee9c..e8ad09b70cd 100644 --- a/ckan/templates/related/confirm_delete.html +++ b/ckan/templates/related/confirm_delete.html @@ -7,7 +7,7 @@ {% block main_content %}
      - {% block form %} + {% block form %}

      {{ _('Are you sure you want to delete related item - {name}?').format(name=c.related_dict.title) }}

      From bf28f1a1a1a0b05fb17f4e8b3e5684f199120709 Mon Sep 17 00:00:00 2001 From: kindly Date: Thu, 5 Sep 2013 16:36:26 +0100 Subject: [PATCH 160/189] add stats and make homepage_style config option --- ckan/controllers/admin.py | 5 +++++ ckan/lib/app_globals.py | 1 + ckan/templates/admin/config.html | 2 ++ ckanext/homepage/plugin.py | 15 +++++++++++++++ .../theme/templates/admin/config.html | 19 ------------------- 5 files changed, 23 insertions(+), 19 deletions(-) delete mode 100644 ckanext/homepage/theme/templates/admin/config.html diff --git a/ckan/controllers/admin.py b/ckan/controllers/admin.py index cb5e84b069b..a8d1f0f257e 100644 --- a/ckan/controllers/admin.py +++ b/ckan/controllers/admin.py @@ -31,6 +31,10 @@ def _get_config_form_items(self): {'text': 'Green', 'value': '/base/css/green.css'}, {'text': 'Maroon', 'value': '/base/css/maroon.css'}, {'text': 'Fuchsia', 'value': '/base/css/fuchsia.css'}] + + homepages = [{'value': '1', 'text': 'Introductory area, search, featured group and featured organization'}, + {'value': '2', 'text': 'Search, featured dataset, stats and featured organization'}] + items = [ {'name': 'ckan.site_title', 'control': 'input', 'label': _('Site Title'), 'placeholder': ''}, {'name': 'ckan.main_css', 'control': 'select', 'options': styles, 'label': _('Style'), 'placeholder': ''}, @@ -39,6 +43,7 @@ def _get_config_form_items(self): {'name': 'ckan.site_about', 'control': 'markdown', 'label': _('About'), 'placeholder': _('About page text')}, {'name': 'ckan.site_intro_text', 'control': 'markdown', 'label': _('Intro Text'), 'placeholder': _('Text on home page')}, {'name': 'ckan.site_custom_css', 'control': 'textarea', 'label': _('Custom CSS'), 'placeholder': _('Customisable css inserted into the page header')}, + {'name': 'ckan.homepage_style', 'control': 'select', 'options': homepages, 'label': _('Homepage'), 'placeholder': ''}, ] return items diff --git a/ckan/lib/app_globals.py b/ckan/lib/app_globals.py index 0739274c671..074deb4e274 100644 --- a/ckan/lib/app_globals.py +++ b/ckan/lib/app_globals.py @@ -46,6 +46,7 @@ 'ckan.dumps_format': {}, 'ckan.api_url': {}, 'ofs.impl': {'name': 'ofs_impl'}, + 'ckan.homepage_style': {'default': '1'}, # split string 'search.facets': {'default': 'organization groups tags res_format license_id', diff --git a/ckan/templates/admin/config.html b/ckan/templates/admin/config.html index 64fd8558b28..5946ce183ef 100644 --- a/ckan/templates/admin/config.html +++ b/ckan/templates/admin/config.html @@ -41,6 +41,8 @@

      <head> tag of every page. If you wish to customize the templates more fully we recommend reading the documentation.

      +

      Homepage: This is for choosing a predefined layout for + the modules that appear on your homepage.

      {% endtrans %} {% endblock %}

      diff --git a/ckanext/homepage/plugin.py b/ckanext/homepage/plugin.py index e59541a16d1..b934f505302 100644 --- a/ckanext/homepage/plugin.py +++ b/ckanext/homepage/plugin.py @@ -1,6 +1,7 @@ import logging import ckan.plugins as p +import ckan.model as model log = logging.getLogger(__name__) @@ -72,8 +73,22 @@ def get_group(id): return groups_data + def get_site_statistics(self): + stats = {} + stats['dataset_count'] = p.toolkit.get_action('package_search')({}, {"rows": 1})['count'] + stats['group_count'] = len(p.toolkit.get_action('group_list')({}, {})) + stats['organization_count'] = len(p.toolkit.get_action('organization_list')({}, {})) + result =model.Session.execute( + '''select count(*) from related r + left join related_dataset rd on r.id = rd.related_id + where rd.status = 'active' or rd.id is null''').first()[0] + stats['related_count'] = len(p.toolkit.get_action('organization_list')({}, {})) + + return stats + def get_helpers(self): return { 'get_featured_organizations': self.get_featured_organizations, 'get_featured_groups': self.get_featured_groups, + 'get_site_statistics': self.get_site_statistics, } diff --git a/ckanext/homepage/theme/templates/admin/config.html b/ckanext/homepage/theme/templates/admin/config.html deleted file mode 100644 index 86e85b47714..00000000000 --- a/ckanext/homepage/theme/templates/admin/config.html +++ /dev/null @@ -1,19 +0,0 @@ -{% ckan_extends %} - -{% import 'macros/form.html' as form %} - -{% block admin_form %} - {{ super() }} - {{ form.select('homepage_layout', label=_('Homepage'), options=[ - {'value': 1, 'text': 'Introductory area, search, featured group and featured organization'}, - {'value': 2, 'text': 'Search, featured dataset, stats and featured organization'} - ], selected=1, error=false) }} -{% endblock %} - -{% block admin_form_help %} - {{ super() }} - {% trans %} -

      Homepage: This is for choosing a predefined layout for - the modules that appear on your homepage.

      - {% endtrans %} -{% endblock %} From 87d0b8b520e1f4860bec04b2a6e5edd67a40b33e Mon Sep 17 00:00:00 2001 From: joetsoi Date: Fri, 6 Sep 2013 14:20:23 +0100 Subject: [PATCH 161/189] [#1228] strip whitespace from title in model_dictize.package_dictize --- ckan/lib/dictization/model_dictize.py | 3 +++ ckan/tests/lib/test_dictization.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/ckan/lib/dictization/model_dictize.py b/ckan/lib/dictization/model_dictize.py index 793f0733c95..c55908fad25 100644 --- a/ckan/lib/dictization/model_dictize.py +++ b/ckan/lib/dictization/model_dictize.py @@ -209,6 +209,9 @@ def package_dictize(pkg, context): if not result: raise logic.NotFound result_dict = d.table_dictize(result, context) + #strip whitespace from title + if result_dict.get('title'): + result_dict['title'] = result_dict['title'].strip() #resources res_rev = model.resource_revision_table resource_group = model.resource_group_table diff --git a/ckan/tests/lib/test_dictization.py b/ckan/tests/lib/test_dictization.py index fb8bb49e628..a2ea317f18a 100644 --- a/ckan/tests/lib/test_dictization.py +++ b/ckan/tests/lib/test_dictization.py @@ -1222,3 +1222,21 @@ def test_25_user_dictize_as_anonymous(self): # Passwords should never be available assert 'password' not in user_dict + + def test_26_package_dictize_whitespace_strippped_from_title(self): + + context = {"model": model, + "session": model.Session} + + pkg = model.Session.query(model.Package).first() + original_title = pkg.title + pkg.title = " whitespace title \t" + model.Session.add(pkg) + model.Session.commit() + + result = package_dictize(pkg, context) + assert result['title'] == 'whitespace title' + pkg.title = original_title + model.Session.add(pkg) + model.Session.commit() + From 0c8bbd171f6b919de112f1f33045c0475aae6402 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 10 Sep 2013 16:43:53 +0100 Subject: [PATCH 162/189] [#1126] Added stats and new layouts 2 and 3 for homepage --- ckan/controllers/admin.py | 3 +- ckan/controllers/home.py | 2 +- ckan/public/base/less/homepage.less | 51 +++++++++++++++---- .../theme/templates/home/layout2.html | 9 ++-- .../theme/templates/home/layout3.html | 23 +++++++++ .../theme/templates/home/snippets/stats.html | 33 ++++++++++++ 6 files changed, 106 insertions(+), 15 deletions(-) create mode 100644 ckanext/homepage/theme/templates/home/layout3.html create mode 100644 ckanext/homepage/theme/templates/home/snippets/stats.html diff --git a/ckan/controllers/admin.py b/ckan/controllers/admin.py index a8d1f0f257e..aec37b5b2c2 100644 --- a/ckan/controllers/admin.py +++ b/ckan/controllers/admin.py @@ -33,7 +33,8 @@ def _get_config_form_items(self): {'text': 'Fuchsia', 'value': '/base/css/fuchsia.css'}] homepages = [{'value': '1', 'text': 'Introductory area, search, featured group and featured organization'}, - {'value': '2', 'text': 'Search, featured dataset, stats and featured organization'}] + {'value': '2', 'text': 'Search, stats, introductory area, featured organization and featured group'}, + {'value': '3', 'text': 'Search, introductory area and stats'}] items = [ {'name': 'ckan.site_title', 'control': 'input', 'label': _('Site Title'), 'placeholder': ''}, diff --git a/ckan/controllers/home.py b/ckan/controllers/home.py index 65f53ca5859..182ced91248 100644 --- a/ckan/controllers/home.py +++ b/ckan/controllers/home.py @@ -177,7 +177,7 @@ def db_to_form_schema(group_type=None): # END OF DIRTYNESS - template = 'home/layout1.html' + template = 'home/layout{0}.html'.format(g.homepage_style) return base.render(template, cache_force=True) def license(self): diff --git a/ckan/public/base/less/homepage.less b/ckan/public/base/less/homepage.less index e53ad89ef6f..3315dfd7772 100644 --- a/ckan/public/base/less/homepage.less +++ b/ckan/public/base/less/homepage.less @@ -2,6 +2,7 @@ [role=main] { padding: 20px 0; + min-height: 0; } .row { @@ -10,7 +11,7 @@ .module-search { padding: 5px; - margin-bottom: 0; + margin: 0; color: @mastheadTextColor; background: @layoutTrimBackgroundColor; .search-giant { @@ -53,17 +54,49 @@ margin: 0; } - &.layout-1 { - .row1 .col2 { - position: absolute; + .box .inner { + padding: @gutterY @gutterX; + } + + .stats { + h3 { + margin: 0 0 10px 0; + } + ul { + .unstyled; + .clearfix; + li { + float: left; + width: 25%; + font-weight: 300; + a { + display: block; + b { + display: block; + font-size: @fontSizeLarge*2; + line-height: 1.5; + } + &:hover { + text-decoration: none; + } + } + } + } + } + + &.layout-1 .row1 .col2 { + position: absolute; + bottom: 0; + right: 0; + .module-search { bottom: 0; + left: 0; right: 0; - .module-search { - bottom: 0; - left: 0; - right: 0; - } } } + &.layout-2 .stats { + margin-top: @gutterY; + } + } diff --git a/ckanext/homepage/theme/templates/home/layout2.html b/ckanext/homepage/theme/templates/home/layout2.html index 99653aa0d86..8ccf432533a 100644 --- a/ckanext/homepage/theme/templates/home/layout2.html +++ b/ckanext/homepage/theme/templates/home/layout2.html @@ -8,9 +8,10 @@
      {% snippet 'home/snippets/search.html' %} + {% snippet 'home/snippets/stats.html' %}
      - {% snippet 'home/snippets/featured_dataset.html' %} + {% snippet 'home/snippets/promoted.html' %}
    @@ -18,12 +19,12 @@
    -
    - {% snippet 'home/snippets/stats.html' %} -
    {% snippet 'home/snippets/featured_organization.html' %}
    +
    + {% snippet 'home/snippets/featured_group.html' %} +
    diff --git a/ckanext/homepage/theme/templates/home/layout3.html b/ckanext/homepage/theme/templates/home/layout3.html new file mode 100644 index 00000000000..4e805845b21 --- /dev/null +++ b/ckanext/homepage/theme/templates/home/layout3.html @@ -0,0 +1,23 @@ +{% extends 'home/index.html' %} + +{% block homepage_layout_class %} layout-3{% endblock %} + +{% block primary_content %} +
    +
    + {% snippet 'home/snippets/search.html' %} +
    +
    +
    +
    +
    +
    + {% snippet 'home/snippets/promoted.html' %} +
    +
    + {% snippet 'home/snippets/stats.html' %} +
    +
    +
    +
    +{% endblock %} diff --git a/ckanext/homepage/theme/templates/home/snippets/stats.html b/ckanext/homepage/theme/templates/home/snippets/stats.html new file mode 100644 index 00000000000..9c5c7fa6970 --- /dev/null +++ b/ckanext/homepage/theme/templates/home/snippets/stats.html @@ -0,0 +1,33 @@ +{% set stats = h.get_site_statistics() %} + + From 569987d68ef1d4c63d71e48eb12a798cac57a59f Mon Sep 17 00:00:00 2001 From: Dominik Moritz Date: Tue, 10 Sep 2013 23:02:07 -0700 Subject: [PATCH 163/189] [#1235] Add image block to be able to replace only the image easily --- ckan/templates/snippets/organization.html | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/ckan/templates/snippets/organization.html b/ckan/templates/snippets/organization.html index 116cb6e3ac4..ef08a9ed440 100644 --- a/ckan/templates/snippets/organization.html +++ b/ckan/templates/snippets/organization.html @@ -20,11 +20,13 @@

    {{ _('Organization') }}

    {% endif %}
    -
    - - {{ organization.name }} - -
    + {% block image %} +
    + + {{ organization.name }} + +
    + {% endblock %}

    {{ organization.title or organization.name }}

    {% if organization.description %}

    From 50acd34cb95d73adb7213d2c6be54fe4ff71e2a0 Mon Sep 17 00:00:00 2001 From: Dominik Moritz Date: Tue, 10 Sep 2013 23:07:08 -0700 Subject: [PATCH 164/189] [#1235] Add block for content of container to allow people to add something to the left of the account block --- ckan/templates/header.html | 100 +++++++++++++++++++------------------ 1 file changed, 51 insertions(+), 49 deletions(-) diff --git a/ckan/templates/header.html b/ckan/templates/header.html index 5b4ebe225a7..da960e89db7 100644 --- a/ckan/templates/header.html +++ b/ckan/templates/header.html @@ -2,58 +2,60 @@ {% block header_account %}

    {% endblock %} From 847320bad8d0ef91e930d090f04b0640c4719e52 Mon Sep 17 00:00:00 2001 From: John Martin Date: Wed, 11 Sep 2013 14:09:59 +0100 Subject: [PATCH 165/189] [#1238] Correctly closes the span on --- ckan/lib/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py index e3ff87f3f32..c70d74d7dbc 100644 --- a/ckan/lib/helpers.py +++ b/ckan/lib/helpers.py @@ -1643,7 +1643,7 @@ def SI_number_span(number): output = literal('') else: output = literal('') - return output + formatters.localised_SI_number(number) + literal('') + return output + formatters.localised_SI_number(number) + literal('') # add some formatter functions localised_number = formatters.localised_number From cab8f8008edb6781a6d053df4095a8e23ef8550a Mon Sep 17 00:00:00 2001 From: John Martin Date: Thu, 12 Sep 2013 12:48:45 +0100 Subject: [PATCH 166/189] [#1214] Removed unecessary closed div as it was breaking the templates --- ckan/templates/organization/member_new.html | 1 - 1 file changed, 1 deletion(-) diff --git a/ckan/templates/organization/member_new.html b/ckan/templates/organization/member_new.html index dd2c4e50bfa..8c1a21c6751 100644 --- a/ckan/templates/organization/member_new.html +++ b/ckan/templates/organization/member_new.html @@ -38,7 +38,6 @@

    {% endblock %} - {% endblock %} {% block secondary_content %} From fea743c9289371ecbbb9c2d013d8f176ed8f4b27 Mon Sep 17 00:00:00 2001 From: Vitor Baptista Date: Thu, 12 Sep 2013 16:21:03 -0300 Subject: [PATCH 167/189] Fix bug in (group|organization)_member_create According to the documentation, the "username" parameter accepts either name or id, but the code only accepted name. I've fixed it to accept both. --- ckan/logic/action/create.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/logic/action/create.py b/ckan/logic/action/create.py index fcb55ceae64..baac5a2154c 100644 --- a/ckan/logic/action/create.py +++ b/ckan/logic/action/create.py @@ -1141,7 +1141,7 @@ def _group_or_org_member_create(context, data_dict, is_org=False): role = data_dict.get('role') group_id = data_dict.get('id') group = model.Group.get(group_id) - result = session.query(model.User).filter_by(name=username).first() + result = model.User.get(username) if result: user_id = result.id else: From 34e73caa8aabb3aa35c3fcea46c1e8ac41599f77 Mon Sep 17 00:00:00 2001 From: Nigel Babu Date: Fri, 13 Sep 2013 12:08:21 +0530 Subject: [PATCH 168/189] Don't modify the list that's being iterated --- ckan/logic/action/delete.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ckan/logic/action/delete.py b/ckan/logic/action/delete.py index e8ba6aeb361..60ea1a5bed8 100644 --- a/ckan/logic/action/delete.py +++ b/ckan/logic/action/delete.py @@ -73,10 +73,9 @@ def resource_delete(context, data_dict): pkg_dict = _get_action('package_show')(context, {'id': package_id}) - for res in pkg_dict.get('resources', []): - if res['id'] == id: - pkg_dict['resources'].remove(res) - break + if pkg_dict.get('resources'): + pkg_dict['resources'] = [r for r in pkg_dict['resources'] if not + r['id'] == id] try: pkg_dict = _get_action('package_update')(context, pkg_dict) except ValidationError, e: From 690bbaeea6b058a078da5212751d98cd790dc1dd Mon Sep 17 00:00:00 2001 From: John Martin Date: Thu, 19 Sep 2013 14:42:36 +0100 Subject: [PATCH 169/189] [#1236] Changes header on sidebar to be more relevant to content --- ckan/templates/user/login.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/templates/user/login.html b/ckan/templates/user/login.html index c84e3522748..a00796b4586 100644 --- a/ckan/templates/user/login.html +++ b/ckan/templates/user/login.html @@ -29,7 +29,7 @@

    {{ _('Need an Account?') }}

    -

    {{ _('Forgotten your details?') }}

    +

    {{ _('Forgotten your password?') }}

    {% trans %}No problem, use our password recovery form to reset it.{% endtrans %}

    From d1e8b9d40e864520ad26425eada1a7af0595390b Mon Sep 17 00:00:00 2001 From: John Martin Date: Thu, 19 Sep 2013 14:50:14 +0100 Subject: [PATCH 170/189] [#1187] Adds source URL and version to package form --- .../package/snippets/package_metadata_fields.html | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ckan/templates/package/snippets/package_metadata_fields.html b/ckan/templates/package/snippets/package_metadata_fields.html index fce09b1d8bc..4f2da316cba 100644 --- a/ckan/templates/package/snippets/package_metadata_fields.html +++ b/ckan/templates/package/snippets/package_metadata_fields.html @@ -2,6 +2,14 @@ {% block package_metadata_fields %} +{% block package_metadata_fields_url %} + {{ form.input('url', label=_('Source'), id='field-url', placeholder=_('http://example.com/dataset.json'), value=data.url, error=errors.url, classes=['control-medium']) }} +{% endblock %} + +{% block package_metadata_fields_version %} + {{ form.input('version', label=_('Version'), id='field-version', placeholder=_('1.0'), value=data.version, error=errors.version, classes=['control-medium']) }} +{% endblock %} + {% block package_metadata_author %} {{ form.input('author', label=_('Author'), id='field-author', placeholder=_('Joe Bloggs'), value=data.author, error=errors.author, classes=['control-medium']) }} From 736a614980f6876d632e0724434d77f93dc7e00a Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Mon, 23 Sep 2013 13:46:31 +0200 Subject: [PATCH 171/189] [#1117] Remove testing guidelines about assert messages This is unnecessary in most cases, should be clear from the test name (or else the test may be doing too much) --- doc/testing-coding-standards.rst | 9 --------- 1 file changed, 9 deletions(-) diff --git a/doc/testing-coding-standards.rst b/doc/testing-coding-standards.rst index 81c9a0026f0..e5082a28de6 100644 --- a/doc/testing-coding-standards.rst +++ b/doc/testing-coding-standards.rst @@ -78,15 +78,6 @@ Clear * Test methods and helper functions should have docstrings. - * Assert statements in tests should have assert messages to explain why the - test is doing this assert, e.g.:: - - assert result['success'] is False, ("Users shouldn't be able to update " - "other users' accounts") - - Assert messages are printed out when tests fail, so the user can often see - what went wrong without even having to look into the test file. - Easy to find It should be easy to know where to add new tests for some new or changed code, or to find the existing tests for some code. From c5ea35f1db74e228aa1e47dbe92ca6db0b6c5ec4 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Mon, 23 Sep 2013 14:22:27 +0200 Subject: [PATCH 172/189] [#1117] Add factories.MockUser class --- ckan/new_tests/factories.py | 40 ++++++++++++++++++++++++ ckan/new_tests/logic/auth/test_update.py | 25 +++------------ 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/ckan/new_tests/factories.py b/ckan/new_tests/factories.py index 639c348a67b..6e8385eeacf 100644 --- a/ckan/new_tests/factories.py +++ b/ckan/new_tests/factories.py @@ -37,6 +37,7 @@ ''' import factory +import mock import ckan.model import ckan.logic @@ -49,6 +50,18 @@ def _generate_email(user): return '{0}@ckan.org'.format(user.name).lower() +def _generate_reset_key(user): + '''Return a reset key for the given User factory stub object.''' + + return '{0}_reset_key'.format(user.name).lower() + + +def _generate_user_id(user): + '''Return a user id for the given User factory stub object.''' + + return '{0}_user_id'.format(user.name).lower() + + class User(factory.Factory): '''A factory class for creating CKAN users.''' @@ -86,6 +99,33 @@ def _create(cls, target_class, *args, **kwargs): return user_dict +class MockUser(factory.Factory): + '''A factory class for creating mock CKAN users using the mock library.''' + + FACTORY_FOR = mock.MagicMock + + fullname = 'Mr. Mock User' + password = 'pass' + about = 'Just another mock user.' + name = factory.Sequence(lambda n: 'mock_user_{n}'.format(n=n)) + email = factory.LazyAttribute(_generate_email) + reset_key = factory.LazyAttribute(_generate_reset_key) + id = factory.LazyAttribute(_generate_user_id) + + @classmethod + def _build(cls, target_class, *args, **kwargs): + raise NotImplementedError(".build() isn't supported in CKAN") + + @classmethod + def _create(cls, target_class, *args, **kwargs): + if args: + assert False, "Positional args aren't supported, use keyword args." + mock_user = mock.MagicMock() + for name, value in kwargs.items(): + setattr(mock_user, name, value) + return mock_user + + def validator_data_dict(): '''Return a data dict with some arbitrary data in it, suitable to be passed to validator functions for testing. diff --git a/ckan/new_tests/logic/auth/test_update.py b/ckan/new_tests/logic/auth/test_update.py index a29e3220b3f..0970e2ed0a3 100644 --- a/ckan/new_tests/logic/auth/test_update.py +++ b/ckan/new_tests/logic/auth/test_update.py @@ -4,6 +4,7 @@ import mock import ckan.new_tests.helpers as helpers +import ckan.new_tests.factories as factories class TestUpdate(object): @@ -12,11 +13,7 @@ def test_user_update_visitor_cannot_update_user(self): '''Visitors should not be able to update users' accounts.''' # Make a mock ckan.model.User object, Fred. - fred = mock.MagicMock() - # Give the mock user object some attributes it's going to need. - fred.reset_key = 'fred_reset_key' - fred.id = 'fred_user_id' - fred.name = 'fred' + fred = factories.MockUser(name='fred') # Make a mock ckan.model object. mock_model = mock.MagicMock() @@ -51,11 +48,7 @@ def test_user_update_user_cannot_update_another_user(self): # 1. Setup. # Make a mock ckan.model.User object, Fred. - fred = mock.MagicMock() - # Give the mock user object some attributes it's going to need. - fred.reset_key = 'fred_reset_key' - fred.id = 'fred_user_id' - fred.name = 'fred' + fred = factories.MockUser(name='fred') # Make a mock ckan.model object. mock_model = mock.MagicMock() @@ -93,11 +86,7 @@ def test_user_update_user_can_update_herself(self): '''Users should be authorized to update their own accounts.''' # Make a mock ckan.model.User object, Fred. - fred = mock.MagicMock() - # Give the mock user object some attributes it's going to need. - fred.reset_key = 'fred_reset_key' - fred.id = 'fred_user_id' - fred.name = 'fred' + fred = factories.MockUser(name='fred') # Make a mock ckan.model object. mock_model = mock.MagicMock() @@ -125,11 +114,7 @@ def test_user_update_user_can_update_herself(self): def test_user_update_with_no_user_in_context(self): # Make a mock ckan.model.User object. - mock_user = mock.MagicMock() - # Give the mock user object some attributes it's going to need. - mock_user.reset_key = 'mock reset key' - mock_user.id = 'mock user id' - mock_user.name = 'mock_user' + mock_user = factories.MockUser(name='fred') # Make a mock ckan.model object. mock_model = mock.MagicMock() From e50a79c7382a400d5491e8f7af91c55f6ee8c93a Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Mon, 23 Sep 2013 14:31:29 +0200 Subject: [PATCH 173/189] [#1117] Remove an unnecessary deepcopy --- ckan/new_tests/lib/navl/test_validators.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ckan/new_tests/lib/navl/test_validators.py b/ckan/new_tests/lib/navl/test_validators.py index c2045a42587..c1fd96010b7 100644 --- a/ckan/new_tests/lib/navl/test_validators.py +++ b/ckan/new_tests/lib/navl/test_validators.py @@ -138,8 +138,6 @@ def call_and_assert(key, data, errors, context=None): if key in original_data_dict: del original_data_dict[key] result = validator(key, data, errors, context=context) - # Copy the data dict because we don't want to modify it. - data = copy.deepcopy(data) assert data == original_data_dict, message return result return call_and_assert From 084feee3bc7b94f70ec6282d90ba122b74342c1c Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Mon, 23 Sep 2013 15:02:46 +0200 Subject: [PATCH 174/189] [#1117] Simplify a test decorator --- ckan/new_tests/lib/navl/test_validators.py | 39 ++++++++++------------ 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/ckan/new_tests/lib/navl/test_validators.py b/ckan/new_tests/lib/navl/test_validators.py index c1fd96010b7..3d0f04809e7 100644 --- a/ckan/new_tests/lib/navl/test_validators.py +++ b/ckan/new_tests/lib/navl/test_validators.py @@ -110,38 +110,36 @@ def call_and_assert(key, data, errors, context=None): return decorator -def does_not_modify_other_keys_in_data_dict(message): +def does_not_modify_other_keys_in_data_dict(validator): '''A decorator that asserts that the decorated validator doesn't add, modify the value of, or remove any other keys from its ``data`` dict param. The function *may* modify its own data dict key. - :param message: the message that will be printed if the function does - modify another key in the data dict and the assert fails + :param validator: the validator function to decorate :type message: string Usage: - @does_not_modify_other_keys_in_data_dict('user_name_validator() ' - 'should not modify other keys in the data dict') + @does_not_modify_other_keys_in_data_dict def call_validator(*args, **kwargs): return validators.user_name_validator(*args, **kwargs) call_validator(key, data, errors) ''' - def decorator(validator): - def call_and_assert(key, data, errors, context=None): - if context is None: - context = {} - # Make a copy of the data dict so we can assert against it later. - original_data_dict = copy.deepcopy(data) - if key in original_data_dict: - del original_data_dict[key] - result = validator(key, data, errors, context=context) - assert data == original_data_dict, message - return result - return call_and_assert - return decorator + def call_and_assert(key, data, errors, context=None): + if context is None: + context = {} + # Make a copy of the data dict so we can assert against it later. + original_data_dict = copy.deepcopy(data) + if key in original_data_dict: + del original_data_dict[key] + result = validator(key, data, errors, context=context) + assert data == original_data_dict, ( + "Should not modify other keys in data_dict, when given a " + "value of {value}".format(value=repr(value))) + return result + return call_and_assert def does_not_modify_errors_dict(message): @@ -201,10 +199,7 @@ def test_ignore_missing_with_value_missing(self): errors = factories.validator_errors_dict() errors[key] = [] - @does_not_modify_other_keys_in_data_dict( - 'When given a value of {value} ignore_missing() should ' - 'not modify other items in the data dict'.format( - value=repr(value))) + @does_not_modify_other_keys_in_data_dict @does_not_modify_errors_dict( 'When given a value of {value} ignore_missing() should not ' 'modify the errors dict'.format(value=repr(value))) From 696f98b392d659211d4c04f60418bc206462a46d Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Mon, 23 Sep 2013 16:23:49 +0200 Subject: [PATCH 175/189] [#1117] Simplify some more test decorators --- ckan/new_tests/lib/navl/test_validators.py | 199 ++++++++++++--------- 1 file changed, 115 insertions(+), 84 deletions(-) diff --git a/ckan/new_tests/lib/navl/test_validators.py b/ckan/new_tests/lib/navl/test_validators.py index 3d0f04809e7..dccee431453 100644 --- a/ckan/new_tests/lib/navl/test_validators.py +++ b/ckan/new_tests/lib/navl/test_validators.py @@ -9,35 +9,41 @@ import ckan.new_tests.factories as factories -def returns_None(message): +def returns_None(function): '''A decorator that asserts that the decorated function returns None. - :param message: the message that will be printed if the function doesn't - return None and the assert fails - :type message: string + :param function: the function to decorate + :type function: function Usage: - @returns_None('user_name_validator() should return None when given ' - 'valid input') + @returns_None def call_validator(*args, **kwargs): return validators.user_name_validator(*args, **kwargs) call_validator(key, data, errors) ''' - def decorator(function): - def call_and_assert(*args, **kwargs): - result = function(*args, **kwargs) - assert result is None, message - return result - return call_and_assert - return decorator + def call_and_assert(*args, **kwargs): + original_args = copy.deepcopy(args) + original_kwargs = copy.deepcopy(kwargs) + + result = function(*args, **kwargs) + + assert result is None, ( + 'Should return None when called with args: {args} and ' + 'kwargs: {kwargs}'.format(args=original_args, + kwargs=original_kwargs)) + return result + return call_and_assert def raises_StopOnError(function): '''A decorator that asserts that the decorated function raises dictization_functions.StopOnError. + :param function: the function to decorate + :type function: function + Usage: @raises_StopOnError @@ -52,62 +58,72 @@ def call_and_assert(*args, **kwargs): return call_and_assert -def does_not_modify_data_dict(message): +def does_not_modify_data_dict(validator): '''A decorator that asserts that the decorated validator doesn't modify its `data` dict param. - :param message: the message that will be printed if the function does - modify the data dict and the assert fails - :type message: string + :param validator: the validator function to decorate + :type validator: function Usage: - @does_not_modify_data_dict('user_name_validator() should not modify ' - 'the data dict') + @does_not_modify_data_dict def call_validator(*args, **kwargs): return validators.user_name_validator(*args, **kwargs) call_validator(key, data, errors) ''' - def decorator(validator): - def call_and_assert(key, data, errors, context=None): - if context is None: - context = {} - # Make a copy of the data dict so we can assert against it later. - original_data_dict = copy.deepcopy(data) - result = validator(key, data, errors, context=context) - assert data == original_data_dict, message - return result - return call_and_assert - return decorator - - -def removes_key_from_data_dict(message): + def call_and_assert(key, data, errors, context=None): + if context is None: + context = {} + original_data = copy.deepcopy(data) + original_errors = copy.deepcopy(errors) + original_context = copy.deepcopy(context) + + result = validator(key, data, errors, context=context) + + assert data == original_data, ( + 'Should not modify data dict when called with ' + 'key: {key}, data: {data}, errors: {errors}, ' + 'context: {context}'.format(key=key, data=original_data, + errors=original_errors, + context=original_context)) + return result + return call_and_assert + + +def removes_key_from_data_dict(validator): '''A decorator that asserts that the decorated validator removes its key from the data dict. - :param message: the message that will be printed if the function does not - remove its key and the assert fails - :type message: string + :param validator: the validator function to decorate + :type validator: function Usage: - @removes_key_from_data_dict('user_name_validator() should remove its ' - 'key from the data dict') + @removes_key_from_data_dict def call_validator(*args, **kwargs): return validators.user_name_validator(*args, **kwargs) call_validator(key, data, errors) ''' - def decorator(validator): - def call_and_assert(key, data, errors, context=None): - if context is None: - context = {} - result = validator(key, data, errors, context=context) - assert key not in data, message - return result - return call_and_assert - return decorator + def call_and_assert(key, data, errors, context=None): + if context is None: + context = {} + original_data = copy.deepcopy(data) + original_errors = copy.deepcopy(errors) + original_context = copy.deepcopy(context) + + result = validator(key, data, errors, context=context) + + assert key not in data, ( + 'Should remove key from data dict when called with: ' + 'key: {key}, data: {data}, errors: {errors}, ' + 'context: {context} '.format(key=key, data=original_data, + errors=original_errors, + context=original_context)) + return result + return call_and_assert def does_not_modify_other_keys_in_data_dict(validator): @@ -117,7 +133,7 @@ def does_not_modify_other_keys_in_data_dict(validator): The function *may* modify its own data dict key. :param validator: the validator function to decorate - :type message: string + :type validator: function Usage: @@ -130,46 +146,68 @@ def call_validator(*args, **kwargs): def call_and_assert(key, data, errors, context=None): if context is None: context = {} - # Make a copy of the data dict so we can assert against it later. - original_data_dict = copy.deepcopy(data) - if key in original_data_dict: - del original_data_dict[key] + original_data = copy.deepcopy(data) + original_errors = copy.deepcopy(errors) + original_context = copy.deepcopy(context) + result = validator(key, data, errors, context=context) - assert data == original_data_dict, ( - "Should not modify other keys in data_dict, when given a " - "value of {value}".format(value=repr(value))) + + # The validator function is allowed to modify its own key, so remove + # that key from both dicts for the purposes of the assertions below. + if key in data: + del data[key] + if key in original_data: + del original_data[key] + + assert data.keys() == original_data.keys(), ( + 'Should not add or remove keys from data dict when called with ' + 'key: {key}, data: {data}, errors: {errors}, ' + 'context: {context}'.format(key=key, data=original_data, + errors=original_errors, + context=original_context)) + for key_ in data: + assert data[key_] == original_data[key_], ( + 'Should not modify other keys in data dict when called with ' + 'key: {key}, data: {data}, errors: {errors}, ' + 'context: {context}'.format(key=key, data=original_data, + errors=original_errors, + context=original_context)) return result return call_and_assert -def does_not_modify_errors_dict(message): +def does_not_modify_errors_dict(validator): '''A decorator that asserts that the decorated validator doesn't modify its `errors` dict param. - :param message: the message that will be printed if the function does - modify the errors dict and the assert fails - :type message: string + :param validator: the validator function to decorate + :type validator: function Usage: - @does_not_modify_errors_dict('user_name_validator() should not modify ' - 'the errors dict') + @does_not_modify_errors_dict def call_validator(*args, **kwargs): return validators.user_name_validator(*args, **kwargs) call_validator(key, data, errors) ''' - def decorator(validator): - def call_and_assert(key, data, errors, context=None): - if context is None: - context = {} - # Make a copy of the errors dict so we can assert against it later. - original_errors_dict = copy.deepcopy(errors) - result = validator(key, data, errors, context=context) - assert errors == original_errors_dict, message - return result - return call_and_assert - return decorator + def call_and_assert(key, data, errors, context=None): + if context is None: + context = {} + original_data = copy.deepcopy(data) + original_errors = copy.deepcopy(errors) + original_context = copy.deepcopy(context) + + result = validator(key, data, errors, context=context) + + assert errors == original_errors, ( + 'Should not modify errors dict when called with key: {key}, ' + 'data: {data}, errors: {errors}, ' + 'context: {context}'.format(key=key, data=original_data, + errors=original_errors, + context=original_context)) + return result + return call_and_assert class TestValidators(object): @@ -200,12 +238,8 @@ def test_ignore_missing_with_value_missing(self): errors[key] = [] @does_not_modify_other_keys_in_data_dict - @does_not_modify_errors_dict( - 'When given a value of {value} ignore_missing() should not ' - 'modify the errors dict'.format(value=repr(value))) - @removes_key_from_data_dict( - 'When given a value of {value} ignore_missing() should remove ' - 'the item from the data dict'.format(value=repr(value))) + @does_not_modify_errors_dict + @removes_key_from_data_dict @raises_StopOnError def call_validator(*args, **kwargs): return validators.ignore_missing(*args, **kwargs) @@ -224,12 +258,9 @@ def test_ignore_missing_with_a_value(self): errors = factories.validator_errors_dict() errors[key] = [] - @returns_None('When called with (non-missing) value, ignore_missing() ' - 'should return None') - @does_not_modify_data_dict("ignore_missing() shouldn't modify the " - 'data dict') - @does_not_modify_errors_dict("ignore_missing() shouldn't modify the " - 'errors dict') + @returns_None + @does_not_modify_data_dict + @does_not_modify_errors_dict def call_validator(*args, **kwargs): return validators.ignore_missing(*args, **kwargs) call_validator(key=key, data=data, errors=errors, context={}) From cf157df17944f68efd37be119e23f7cf06f6125b Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Mon, 23 Sep 2013 16:49:59 +0200 Subject: [PATCH 176/189] [#1117] Simplify some more test decorators And also remove some duplicated test decorators --- ckan/new_tests/logic/test_validators.py | 234 ++++++++---------------- 1 file changed, 77 insertions(+), 157 deletions(-) diff --git a/ckan/new_tests/logic/test_validators.py b/ckan/new_tests/logic/test_validators.py index aafda332eaa..301cd3fc540 100644 --- a/ckan/new_tests/logic/test_validators.py +++ b/ckan/new_tests/logic/test_validators.py @@ -8,59 +8,42 @@ import nose.tools import ckan.new_tests.factories as factories - - -def returns_arg(message): +# Import some test helper functions from another module directly. +# This is bad (test modules shouldn't share code with eachother) but because of +# the way validator functions are organised in CKAN (in different modules in +# different places in the code) we have to either do this or introduce a shared +# test helper functions module (which we also don't want to do). +from ckan.new_tests.lib.navl.test_validators import ( + returns_None, + does_not_modify_data_dict, + does_not_modify_errors_dict, + ) + + +def returns_arg(function): '''A decorator that tests that the decorated function returns the argument that it is called with, unmodified. - :param message: the message that will be printed if the function doesn't - return the same argument that it was called with and the assert fails - :type message: string + :param function: the function to decorate + :type function: function Usage: - @returns_arg('user_name_validator() should return the same arg that ' - 'it is called with, when called with a valid arg') + @returns_arg def call_validator(*args, **kwargs): return validators.user_name_validator(*args, **kwargs) call_validator(key, data, errors) ''' - def decorator(function): - def call_and_assert(arg, context=None): - if context is None: - context = {} - result = function(arg, context=context) - assert result == arg, message - return result - return call_and_assert - return decorator - - -def returns_None(message): - '''A decorator that asserts that the decorated function returns None. - - :param message: the message that will be printed if the function doesn't - return None and the assert fails - :type message: string - - Usage: - - @returns_None('user_name_validator() should return None when given ' - 'valid input') - def call_validator(*args, **kwargs): - return validators.user_name_validator(*args, **kwargs) - call_validator(key, data, errors) - - ''' - def decorator(function): - def call_and_assert(*args, **kwargs): - result = function(*args, **kwargs) - assert result is None, message - return result - return call_and_assert - return decorator + def call_and_assert(arg, context=None): + if context is None: + context = {} + result = function(arg, context=context) + assert result == arg, ( + 'Should return the argument that was passed to it, unchanged ' + '({arg})'.format(arg=repr(arg))) + return result + return call_and_assert def raises_Invalid(function): @@ -81,102 +64,58 @@ def call_and_assert(*args, **kwargs): return call_and_assert -def does_not_modify_data_dict(message): - '''A decorator that asserts that the decorated validator doesn't modify - its `data` dict param. - - :param message: the message that will be printed if the function does - modify the data dict and the assert fails - :type message: string - - Usage: - - @does_not_modify_data_dict('user_name_validator() should not modify ' - 'the data dict') - def call_validator(*args, **kwargs): - return validators.user_name_validator(*args, **kwargs) - call_validator(key, data, errors) - - ''' - def decorator(validator): - def call_and_assert(key, data, errors, context=None): - if context is None: - context = {} - # Make a copy of the data dict so we can assert against it later. - original_data_dict = copy.deepcopy(data) - result = validator(key, data, errors, context=context) - assert data == original_data_dict, message - return result - return call_and_assert - return decorator - - -def does_not_modify_errors_dict(message): - '''A decorator that asserts that the decorated validator doesn't modify its - `errors` dict param. - - :param message: the message that will be printed if the function does - modify the errors dict and the assert fails - :type message: string - - Usage: - - @does_not_modify_errors_dict('user_name_validator() should not modify ' - 'the errors dict') - def call_validator(*args, **kwargs): - return validators.user_name_validator(*args, **kwargs) - call_validator(key, data, errors) - - ''' - def decorator(validator): - def call_and_assert(key, data, errors, context=None): - if context is None: - context = {} - # Make a copy of the errors dict so we can assert against it later. - original_errors_dict = copy.deepcopy(errors) - result = validator(key, data, errors, context=context) - assert errors == original_errors_dict, message - return result - return call_and_assert - return decorator - - -def does_not_modify_other_keys_in_errors_dict(message): +def does_not_modify_other_keys_in_errors_dict(validator): '''A decorator that asserts that the decorated validator doesn't add, - modify the value of, or remove any other keys from its `errors` dict param. + modify the value of, or remove any other keys from its ``errors`` dict + param. - The function *may* modify its own errors `key`. + The function *may* modify its own errors dict key. - :param message: the message that will be printed if the function does - modify another key in the errors dict and the assert fails - :type message: string + :param validator: the validator function to decorate + :type validator: function Usage: - @does_not_modify_other_keys_in_errors_dict('user_name_validator() ' - 'should not modify other keys in the errors dict') + @does_not_modify_other_keys_in_errors_dict def call_validator(*args, **kwargs): return validators.user_name_validator(*args, **kwargs) call_validator(key, data, errors) ''' - def decorator(validator): - def call_and_assert(key, data, errors, context=None): - if context is None: - context = {} - # Make a copy of the errors dict so we can assert against it later. - original_errors_dict = copy.deepcopy(errors) - result = validator(key, data, errors, context=context) - # Copy the errors dict because we don't want to modify it. - errors = copy.deepcopy(errors) - errors[key] = [] - assert errors == original_errors_dict, message - return result - return call_and_assert - return decorator + def call_and_assert(key, data, errors, context=None): + if context is None: + context = {} + original_data = copy.deepcopy(data) + original_errors = copy.deepcopy(errors) + original_context = copy.deepcopy(context) + + result = validator(key, data, errors, context=context) + + # The validator function is allowed to modify its own key, so remove + # that key from both dicts for the purposes of the assertions below. + if key in errors: + del errors[key] + if key in original_errors: + del original_errors[key] + + assert errors.keys() == original_errors.keys(), ( + 'Should not add or remove keys from errors dict when called with ' + 'key: {key}, data: {data}, errors: {errors}, ' + 'context: {context}'.format(key=key, data=original_data, + errors=original_errors, + context=original_context)) + for key_ in errors: + assert errors[key_] == original_errors[key_], ( + 'Should not modify other keys in errors dict when called with ' + 'key: {key}, data: {data}, errors: {errors}, ' + 'context: {context}'.format(key=key, data=original_data, + errors=original_errors, + context=original_context)) + return result + return call_and_assert -def adds_message_to_errors_dict(error_message, message): +def adds_message_to_errors_dict(error_message): '''A decorator that asserts the the decorated validator adds a given error message to the `errors` dict. @@ -184,15 +123,9 @@ def adds_message_to_errors_dict(error_message, message): add to the `errors` dict :type error_message: string - :param message: the message that will be printed if the function doesn't - add the right error message to the errors dict, and the assert fails - :type message: string - Usage: - @adds_message_to_errors_dict('That login name is not available.', - 'user_name_validator() should add to the errors dict when called ' - 'with a user name with already exists') + @adds_message_to_errors_dict('That login name is not available.') def call_validator(*args, **kwargs): return validators.user_name_validator(*args, **kwargs) call_validator(key, data, errors) @@ -201,7 +134,9 @@ def call_validator(*args, **kwargs): def decorator(validator): def call_and_assert(key, data, errors, context): result = validator(key, data, errors, context) - assert errors[key] == [error_message], message + assert errors[key] == [error_message], ( + 'Should add message to errors dict: {msg}'.format( + msg=error_message)) return result return call_and_assert return decorator @@ -286,8 +221,7 @@ def test_name_validator_with_valid_value(self): ] for valid_name in valid_names: - @returns_arg('If given a valid string name_validator() should ' - 'return the string unmodified') + @returns_arg def call_validator(*args, **kwargs): return validators.name_validator(*args, **kwargs) call_validator(valid_name) @@ -327,10 +261,7 @@ def test_user_name_validator_with_non_string_value(self): errors = factories.validator_errors_dict() errors[key] = [] - @does_not_modify_errors_dict( - 'user_name_validator() should not modify the errors dict') - @does_not_modify_data_dict( - 'user_name_validator() should not modify the data dict') + @does_not_modify_data_dict @raises_Invalid def call_validator(*args, **kwargs): return validators.user_name_validator(*args, **kwargs) @@ -356,17 +287,10 @@ def test_user_name_validator_with_a_name_that_already_exists(self): errors = factories.validator_errors_dict() errors[key] = [] - @does_not_modify_other_keys_in_errors_dict('user_name_validator() ' - 'should not modify other ' - 'keys in the errors dict') - @does_not_modify_data_dict('user_name_validator() should not modify ' - 'the data dict') - @returns_None('user_name_validator() should return None if called ' - 'with a user name that already exists') - @adds_message_to_errors_dict('That login name is not available.', - 'user_name_validator() should add to the ' - 'errors dict when called with the name ' - 'of a user that already exists') + @does_not_modify_other_keys_in_errors_dict + @does_not_modify_data_dict + @returns_None + @adds_message_to_errors_dict('That login name is not available.') def call_validator(*args, **kwargs): return validators.user_name_validator(*args, **kwargs) call_validator(key, data, errors, context={'model': mock_model}) @@ -388,13 +312,9 @@ def test_user_name_validator_successful(self): # user with that name exists in the database. mock_model.User.get.return_value = None - @does_not_modify_errors_dict('user_name_validator() should not ' - 'modify the errors dict when given ' - 'valid input') - @does_not_modify_data_dict('user_name_validator() should not modify ' - 'the data dict when given valid input') - @returns_None('user_name_validator() should return None when given ' - 'valid input') + @does_not_modify_errors_dict + @does_not_modify_data_dict + @returns_None def call_validator(*args, **kwargs): return validators.user_name_validator(*args, **kwargs) call_validator(key, data, errors, context={'model': mock_model}) From 5ca99eee04dae54c9f9542bb21b1114294365445 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Mon, 23 Sep 2013 17:25:33 +0200 Subject: [PATCH 177/189] [#1117] Tests use clean_db() not delete_all() This fixes crashes when the database was not initialized or was only partially initialized --- ckan/new_tests/helpers.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ckan/new_tests/helpers.py b/ckan/new_tests/helpers.py index 371f5e02787..fece02a18e1 100644 --- a/ckan/new_tests/helpers.py +++ b/ckan/new_tests/helpers.py @@ -38,9 +38,7 @@ def reset_db(): # This prevents CKAN from hanging waiting for some unclosed connection. model.Session.close_all() - # Clean out any data from the db. This prevents tests failing due to data - # leftover from other tests or from previous test runs. - model.repo.delete_all() + model.repo.clean_db() def call_action(action_name, context=None, **kwargs): From 21c21df965609e9dbfd1a20d1e01dc5d44807f34 Mon Sep 17 00:00:00 2001 From: Vitor Baptista Date: Tue, 24 Sep 2013 15:01:23 -0300 Subject: [PATCH 178/189] [#1243] Add tests to group_member_create. --- ckan/tests/logic/test_action.py | 49 +++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/ckan/tests/logic/test_action.py b/ckan/tests/logic/test_action.py index 89e030beac8..9d7bb3c1dbf 100644 --- a/ckan/tests/logic/test_action.py +++ b/ckan/tests/logic/test_action.py @@ -1722,3 +1722,52 @@ def test_2_view_group(self): res_json = json.loads(res.body) assert res_json['success'] is False + +class TestMember(WsgiAppCase): + + sysadmin = None + + group = None + + def setup(self): + username = 'sysadmin' + groupname = 'test group' + organization_name = 'test organization' + CreateTestData.create_user('sysadmin', **{ 'sysadmin': True }) + CreateTestData.create_groups([{ 'name': groupname }, + { 'name': organization_name, + 'type': 'organization'}]) + self.sysadmin = model.User.get(username) + self.group = model.Group.get(groupname) + + def teardown(self): + model.repo.rebuild_db() + + def test_group_member_create_works_user_id_and_group_id(self): + self._assert_we_can_add_user_to_group(self.sysadmin.id, self.group.id) + + def test_group_member_create_works_with_user_id_and_group_name(self): + self._assert_we_can_add_user_to_group(self.sysadmin.id, self.group.name) + + def test_group_member_create_works_with_user_name_and_group_name(self): + self._assert_we_can_add_user_to_group(self.sysadmin.name, self.group.name) + + def _assert_we_can_add_user_to_group(self, user_id, group_id): + user = model.User.get(user_id) + group = model.Group.get(group_id) + url = '/api/action/group_member_create' + role = 'member' + postparams = '%s=1' % json.dumps({ + 'id': group_id, + 'username': user_id, + 'role': role}) + + res = self.app.post(url, params=postparams, + extra_environ={'Authorization': str(user.apikey)}) + + res = json.loads(res.body) + groups = user.get_groups(group.type, role) + group_ids = [g.id for g in groups] + assert res['success'] is True, res + assert group.id in group_ids, (group, user_groups) + From 2d46b36aa53cb96ca37c4040b514935e9abe5ced Mon Sep 17 00:00:00 2001 From: Vitor Baptista Date: Wed, 25 Sep 2013 15:33:59 -0300 Subject: [PATCH 179/189] Correct typo in CHANGELOG, forntend -> frontend. --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 96bee677cd1..b97798a4522 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -93,7 +93,7 @@ Deprecated and removed: one. Please update your config files if using it. (#226) Known issues: - * Under certain authorization setups the forntend for the groups functionality + * Under certain authorization setups the frontend for the groups functionality may not work as expected (See #1176 #1175). From 9147540ab8e381a41ab849239b919c19a2641a35 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Tue, 1 Oct 2013 12:17:54 +0200 Subject: [PATCH 180/189] [#1117] Discourage overuse of mocking Tweak the testing guidelines to discourage overuse of mocking. --- ckan/new_tests/controllers/__init__.py | 1 + ckan/new_tests/logic/action/__init__.py | 3 +- ckan/new_tests/logic/action/test_update.py | 72 ---------------------- doc/testing-coding-standards.rst | 40 ++++++++---- 4 files changed, 30 insertions(+), 86 deletions(-) diff --git a/ckan/new_tests/controllers/__init__.py b/ckan/new_tests/controllers/__init__.py index 6a901790973..916996429f3 100644 --- a/ckan/new_tests/controllers/__init__.py +++ b/ckan/new_tests/controllers/__init__.py @@ -1,4 +1,5 @@ ''' +Controller tests probably shouldn't use mocking. .. todo:: diff --git a/ckan/new_tests/logic/action/__init__.py b/ckan/new_tests/logic/action/__init__.py index d666170367e..6272c5b24b2 100644 --- a/ckan/new_tests/logic/action/__init__.py +++ b/ckan/new_tests/logic/action/__init__.py @@ -3,7 +3,8 @@ Most action function tests will be high-level tests that both test the code in the action function itself, and also indirectly test the code in :mod:`ckan.lib`, :mod:`ckan.model`, :mod:`ckan.logic.schema` etc. that the -action function calls. +action function calls. This means that most action function tests should *not* +use mocking. Tests for action functions should use the :func:`ckan.new_tests.helpers.call_action` function to call the action diff --git a/ckan/new_tests/logic/action/test_update.py b/ckan/new_tests/logic/action/test_update.py index dd32c4f7c37..8413a3a8178 100644 --- a/ckan/new_tests/logic/action/test_update.py +++ b/ckan/new_tests/logic/action/test_update.py @@ -344,75 +344,3 @@ def test_user_update_does_not_return_reset_key(self): updated_user = helpers.call_action('user_update', **params) assert 'reset_key' not in updated_user - - ## START-COMPLEX-MOCK-EXAMPLE - - def test_user_update_with_deferred_commit(self): - '''Test that user_update()'s deferred_commit option works. - - In this test we mock out the rest of CKAN and test the user_update() - action function in isolation. What we're testing is simply that when - called with 'deferred_commit': True in its context, user_update() does - not call ckan.model.repo.commit(). - - ''' - # Patch ckan.model, so user_update will be accessing a mock object - # instead of the real model. - # It's ckan.logic.__init__.py:get_action() that actually adds the model - # into the context dict, and that module does - # `import ckan.model as model`, so what we actually need to patch is - # 'ckan.logic.model' (the name that the model has when get_action() - # accesses it), not 'ckan.model'. - model_patch = mock.patch('ckan.logic.model') - mock_model = model_patch.start() - - # Patch the validate() function, so validate() won't really be called - # when user_update() calls it. update.py does - # `_validate = ckan.lib.navl.dictization_functions.validate` so we - # actually to patch this new name that's assigned to the function, not - # its original name. - validate_patch = mock.patch('ckan.logic.action.update._validate') - mock_validate = validate_patch.start() - - # user_update() is going to call validate() with some params and it's - # going to use the result that validate() returns. So we need to give - # a mock validate function that will accept the right number of params - # and return the right result. - def mock_validate_function(data_dict, schema, context): - '''Simply return the data_dict as given (without doing any - validation) and an empty error dict.''' - return data_dict, {} - mock_validate.side_effect = mock_validate_function - - # Patch model_save, again update.py does - # `import ckan.logic.action.update.model_save as model_save` so we - # need to patch it by its new name - # 'ckan.logic.action.update.model_save'. - model_save_patch = mock.patch('ckan.logic.action.update.model_save') - model_save_patch.start() - - # Patch model_dictize, again using the new name that update.py imports - # it as. - model_dictize_patch = mock.patch( - 'ckan.logic.action.update.model_dictize') - model_dictize_patch.start() - - # Patch the get_action() function (using the name that update.py - # assigns to it) with a default mock function that does nothing and - # returns None. - get_action_patch = mock.patch('ckan.logic.action.update._get_action') - get_action_patch.start() - - # After all that patching, we can finally call user_update passing - # 'defer_commit': True. The logic code in user_update will be run but - # it'll have no effect because everything it calls is mocked out. - helpers.call_action('user_update', - context={'defer_commit': True}, - id='foobar', name='new_name', - ) - - # Assert that user_update did *not* call our mock model object's - # model.repo.commit() method. - assert not mock_model.repo.commit.called - - ## END-COMPLEX-MOCK-EXAMPLE diff --git a/doc/testing-coding-standards.rst b/doc/testing-coding-standards.rst index e5082a28de6..2bd89545da2 100644 --- a/doc/testing-coding-standards.rst +++ b/doc/testing-coding-standards.rst @@ -41,9 +41,6 @@ Guidelines for writing new-style tests We want the tests in :mod:`ckan.new_tests` to be: Fast - * Use the ``mock`` library to avoid pulling in other parts of CKAN - (especially the database), see :ref:`mock`. - * Don't share setup code between tests (e.g. in test class ``setup()`` or ``setup_class()`` methods, saved against the ``self`` attribute of test classes, or in test helper modules). @@ -52,6 +49,9 @@ Fast and have each test method call just the helpers it needs to do the setup that it needs. + * Where appropriate, use the ``mock`` library to avoid pulling in other parts + of CKAN (especially the database), see :ref:`mock`. + Independent * Each test module, class and method should be able to be run on its own. @@ -269,6 +269,30 @@ faster (especially when :py:mod:`ckan.model` is mocked out so that the tests don't touch the database). With mock objects we can also make assertions about what methods the function called on the mock object and with which arguments. +.. note:: + + Overuse of mocking is discouraged as it can make tests difficult to + understand and maintain. Mocking can be useful and make tests both faster + and simpler when used appropriately. Some rules of thumb: + + * Don't mock out more than one or two objects in a single test method. + + * Don't use mocking in more functional-style tests. For example the action + function tests in :py:mod:`ckan.new_tests.logic.action` and the + frontend tests in :py:mod:`ckan.new_tests.controllers` are functional + tests, and probably shouldn't do any mocking. + + * Do use mocking in more unit-style tests. For example the authorization + function tests in :py:mod:`ckan.new_tests.logic.auth`, the converter and + validator tests in :py:mod:`ckan.new_tests.logic.auth`, and most (all?) + lib tests in :py:mod:`ckan.new_tests.lib` are unit tests and should use + mocking when necessary (often it's possible to unit test a method in + isolation from other CKAN code without doing any mocking, which is ideal). + + In these kind of tests we can often mock one or two objects in a simple + and easy to understand way, and make the test both simpler and faster. + + A mock object is a special object that allows user code to access any attribute name or call any method name (and pass any parameters) on the object, and the code will always get another mock object back: @@ -301,16 +325,6 @@ out :py:mod:`ckan.model`: :start-after: ## START-AFTER :end-before: ## END-BEFORE -Here's a much more complex example that patches a number of CKAN modules with -mock modules, replaces CKAN functions with mock functions with mock behavior, -and makes assertions about how CKAN called the mock objects (you shouldn't -have to do mocking as complex as this too often!): - -.. literalinclude:: ../ckan/new_tests/logic/action/test_update.py - :language: python - :start-after: ## START-COMPLEX-MOCK-EXAMPLE - :end-before: ## END-COMPLEX-MOCK-EXAMPLE - ---- The following sections will give specific guidelines and examples for writing From 098ac5de1ece4a5851a2ee4cd4290847f3472aeb Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Tue, 1 Oct 2013 12:18:40 +0200 Subject: [PATCH 181/189] [#1117] Add another example to testing guidelines --- doc/testing-coding-standards.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/testing-coding-standards.rst b/doc/testing-coding-standards.rst index 2bd89545da2..dfc4f19169d 100644 --- a/doc/testing-coding-standards.rst +++ b/doc/testing-coding-standards.rst @@ -187,7 +187,12 @@ give the following recipe for all unit test methods to follow: 3. Make assertions about the return value, and / or any side effects. 4. Do absolutely nothing else. -Most CKAN tests should follow this form. +Most CKAN tests should follow this form. Here's an example of a simple action +function test demonstrating the recipe: + +.. literalinclude:: ../ckan/new_tests/logic/action/test_update.py + :start-after: ## START-AFTER + :end-before: ## END-BEFORE One common exception is when you want to use a ``for`` loop to call the function being tested multiple times, passing it lots of different arguments From af2ed73934b49cece0920266d0dba3b025ac114e Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Tue, 1 Oct 2013 12:19:18 +0200 Subject: [PATCH 182/189] [#1117] Testing guidelines small tweaks --- ckan/new_tests/helpers.py | 8 ++++---- doc/testing-coding-standards.rst | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ckan/new_tests/helpers.py b/ckan/new_tests/helpers.py index fece02a18e1..51640a7e3dd 100644 --- a/ckan/new_tests/helpers.py +++ b/ckan/new_tests/helpers.py @@ -1,6 +1,6 @@ '''This is a collection of helper functions for use in tests. -We want to avoid using sharing test helper functions between test modules as +We want to avoid sharing test helper functions between test modules as much as possible, and we definitely don't want to share test fixtures between test modules, or to introduce a complex hierarchy of test class subclasses, etc. @@ -42,12 +42,12 @@ def reset_db(): def call_action(action_name, context=None, **kwargs): - '''Call the named ``ckan.logic.action`` and return the result. + '''Call the named ``ckan.logic.action`` function and return the result. For example:: - call_action('user_create', name='seanh', email='seanh@seanh.com', - password='pass') + user_dict = call_action('user_create', name='seanh', + email='seanh@seanh.com', password='pass') Any keyword arguments given will be wrapped in a dict and passed to the action function as its ``data_dict`` argument. diff --git a/doc/testing-coding-standards.rst b/doc/testing-coding-standards.rst index dfc4f19169d..8e0b7fd4349 100644 --- a/doc/testing-coding-standards.rst +++ b/doc/testing-coding-standards.rst @@ -219,7 +219,7 @@ How detailed should tests be? Generally, what we're trying to do is test the *interfaces* between modules in a way that supports modularization: if you change the code within a function, -method, class or module, if you don't break any of that code's unit tests you +method, class or module, if you don't break any of that code's tests you should be able to expect that CKAN as a whole will not be broken. As a general guideline, the tests for a function or method should: From 7e61b82eac629c92ba04e0d9567e96b7c709f49b Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Tue, 1 Oct 2013 12:22:40 +0200 Subject: [PATCH 183/189] Fix a Sphinx error --- ckan/plugins/interfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/plugins/interfaces.py b/ckan/plugins/interfaces.py index a75d6e99906..74462835474 100644 --- a/ckan/plugins/interfaces.py +++ b/ckan/plugins/interfaces.py @@ -578,7 +578,7 @@ def user_create(context, data_dict=None): be decorated with the ``auth_allow_anonymous_access`` decorator, available on the plugins toolkit. - For example: + For example:: import ckan.plugins as p From 3fbe844985a00aee1450fd7235ba61bcf293623b Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 1 Oct 2013 11:22:52 +0100 Subject: [PATCH 184/189] [#1126] Removed because it's not used --- ckanext/homepage/plugin.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/ckanext/homepage/plugin.py b/ckanext/homepage/plugin.py index b934f505302..d9a79b48a2c 100644 --- a/ckanext/homepage/plugin.py +++ b/ckanext/homepage/plugin.py @@ -10,9 +10,6 @@ class HomepagePlugin(p.SingletonPlugin): p.implements(p.ITemplateHelpers, inherit=True) p.implements(p.IConfigurable, inherit=True) - featured_groups_cache = None - featured_orgs_cache = None - def configure(self, config): groups = config.get('ckan.featured_groups', '') if groups: @@ -29,16 +26,14 @@ def get_featured_organizations(self, count=1): list_action='organization_list', count=count, items=self.orgs) - self.featured_orgs_cache = orgs - return self.featured_orgs_cache + return orgs def get_featured_groups(self, count=1): groups = self.featured_group_org(get_action='group_show', list_action='group_list', count=count, items=self.groups) - self.featured_groups_cache = groups - return self.featured_groups_cache + return groups def featured_group_org(self, items, get_action, list_action, count): def get_group(id): From 66645627a9ac2a23cc0335e4ae3766d60e0aedc3 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Tue, 1 Oct 2013 16:17:29 +0200 Subject: [PATCH 185/189] [#1117] Fix an import (PEP8) --- ckan/new_tests/logic/test_validators.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/ckan/new_tests/logic/test_validators.py b/ckan/new_tests/logic/test_validators.py index 301cd3fc540..4aab0fad23c 100644 --- a/ckan/new_tests/logic/test_validators.py +++ b/ckan/new_tests/logic/test_validators.py @@ -8,16 +8,12 @@ import nose.tools import ckan.new_tests.factories as factories -# Import some test helper functions from another module directly. +# Import some test helper functions from another module. # This is bad (test modules shouldn't share code with eachother) but because of # the way validator functions are organised in CKAN (in different modules in # different places in the code) we have to either do this or introduce a shared # test helper functions module (which we also don't want to do). -from ckan.new_tests.lib.navl.test_validators import ( - returns_None, - does_not_modify_data_dict, - does_not_modify_errors_dict, - ) +import ckan.new_tests.lib.navl.test_validators as t def returns_arg(function): @@ -261,7 +257,7 @@ def test_user_name_validator_with_non_string_value(self): errors = factories.validator_errors_dict() errors[key] = [] - @does_not_modify_data_dict + @t.does_not_modify_data_dict @raises_Invalid def call_validator(*args, **kwargs): return validators.user_name_validator(*args, **kwargs) @@ -288,8 +284,8 @@ def test_user_name_validator_with_a_name_that_already_exists(self): errors[key] = [] @does_not_modify_other_keys_in_errors_dict - @does_not_modify_data_dict - @returns_None + @t.does_not_modify_data_dict + @t.returns_None @adds_message_to_errors_dict('That login name is not available.') def call_validator(*args, **kwargs): return validators.user_name_validator(*args, **kwargs) @@ -312,9 +308,9 @@ def test_user_name_validator_successful(self): # user with that name exists in the database. mock_model.User.get.return_value = None - @does_not_modify_errors_dict - @does_not_modify_data_dict - @returns_None + @t.does_not_modify_errors_dict + @t.does_not_modify_data_dict + @t.returns_None def call_validator(*args, **kwargs): return validators.user_name_validator(*args, **kwargs) call_validator(key, data, errors, context={'model': mock_model}) From ad55287efb4a706ad4e957bc43cf7b019ad3b3c6 Mon Sep 17 00:00:00 2001 From: kindly Date: Wed, 2 Oct 2013 09:38:54 +0100 Subject: [PATCH 186/189] [1126] move homepage modules into core and fix tests --- ckan/controllers/home.py | 3 +- ckan/lib/app_globals.py | 1 + ckan/lib/helpers.py | 71 +++++++++++++++ ckan/templates/home/index.html | 16 +--- ckan/templates/home/index_base.html | 15 ++++ .../templates/home/layout1.html | 2 +- .../templates/home/layout2.html | 2 +- .../templates/home/layout3.html | 2 +- .../home/snippets/featured_group.html | 0 .../home/snippets/featured_organization.html | 0 .../templates/home/snippets/promoted.html | 0 .../templates/home/snippets/search.html | 0 .../templates/home/snippets/stats.html | 0 ckanext/homepage/__init__.py | 0 ckanext/homepage/plugin.py | 89 ------------------- doc/configuration.rst | 14 +++ setup.py | 1 - 17 files changed, 106 insertions(+), 110 deletions(-) create mode 100644 ckan/templates/home/index_base.html rename {ckanext/homepage/theme => ckan}/templates/home/layout1.html (95%) rename {ckanext/homepage/theme => ckan}/templates/home/layout2.html (95%) rename {ckanext/homepage/theme => ckan}/templates/home/layout3.html (93%) rename {ckanext/homepage/theme => ckan}/templates/home/snippets/featured_group.html (100%) rename {ckanext/homepage/theme => ckan}/templates/home/snippets/featured_organization.html (100%) rename {ckanext/homepage/theme => ckan}/templates/home/snippets/promoted.html (100%) rename {ckanext/homepage/theme => ckan}/templates/home/snippets/search.html (100%) rename {ckanext/homepage/theme => ckan}/templates/home/snippets/stats.html (100%) delete mode 100644 ckanext/homepage/__init__.py delete mode 100644 ckanext/homepage/plugin.py diff --git a/ckan/controllers/home.py b/ckan/controllers/home.py index ad34bd702ca..da3d4b3901b 100644 --- a/ckan/controllers/home.py +++ b/ckan/controllers/home.py @@ -179,8 +179,7 @@ def db_to_form_schema(group_type=None): # END OF DIRTYNESS - template = 'home/layout{0}.html'.format(g.homepage_style) - return base.render(template, cache_force=True) + return base.render('home/index.html', cache_force=True) def license(self): return base.render('home/license.html') diff --git a/ckan/lib/app_globals.py b/ckan/lib/app_globals.py index 074deb4e274..dde98f0a1ed 100644 --- a/ckan/lib/app_globals.py +++ b/ckan/lib/app_globals.py @@ -29,6 +29,7 @@ 'ckan.site_about', 'ckan.site_intro_text', 'ckan.site_custom_css', + 'ckan.homepage_style', ] config_details = { diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py index c70d74d7dbc..94e7c97b0e6 100644 --- a/ckan/lib/helpers.py +++ b/ckan/lib/helpers.py @@ -1663,6 +1663,74 @@ def new_activities(): action = logic.get_action('dashboard_new_activities_count') return action({}, {}) +def get_featured_organizations(count=1): + '''Returns a list of favourite organization in the form + of organization_list action function + ''' + config_orgs = config.get('ckan.featured_orgs', '').split() + orgs = featured_group_org(get_action='organization_show', + list_action='organization_list', + count=count, + items=config_orgs) + return orgs + +def get_featured_groups(count=1): + '''Returns a list of favourite group the form + of organization_list action function + ''' + config_groups = config.get('ckan.featured_groups', '').split() + groups = featured_group_org(get_action='group_show', + list_action='group_list', + count=count, + items=config_groups) + return groups + +def featured_group_org(items, get_action, list_action, count): + def get_group(id): + context = {'ignore_auth': True, + 'limits': {'packages': 2}, + 'for_view': True} + data_dict = {'id': id} + + try: + out = logic.get_action(get_action)(context, data_dict) + except logic.ObjectNotFound: + return None + return out + + groups_data = [] + + extras = logic.get_action(list_action)({}, {}) + + # list of found ids to prevent duplicates + found = [] + for group_name in items + extras: + group = get_group(group_name) + if not group: + continue + # ckeck if duplicate + if group['id'] in found: + continue + found.append(group['id']) + groups_data.append(group) + if len(groups_data) == count: + break + + return groups_data + +def get_site_statistics(): + stats = {} + stats['dataset_count'] = logic.get_action('package_search')({}, {"rows": 1})['count'] + stats['group_count'] = len(logic.get_action('group_list')({}, {})) + stats['organization_count'] = len(logic.get_action('organization_list')({}, {})) + result =model.Session.execute( + '''select count(*) from related r + left join related_dataset rd on r.id = rd.related_id + where rd.status = 'active' or rd.id is null''').first()[0] + stats['related_count'] = result + + return stats + # these are the functions that will end up in `h` template helpers __allowed_functions__ = [ @@ -1761,4 +1829,7 @@ def new_activities(): 'radio', 'submit', 'asbool', + 'get_featured_organizations', + 'get_featured_groups', + 'get_site_statistics', ] diff --git a/ckan/templates/home/index.html b/ckan/templates/home/index.html index d9e4abfb17b..8684c551732 100644 --- a/ckan/templates/home/index.html +++ b/ckan/templates/home/index.html @@ -1,15 +1 @@ -{% extends "page.html" %} - -{% block subtitle %}{{ _("Welcome") }}{% endblock %} - -{% block maintag %}{% endblock %} -{% block toolbar %}{% endblock %} - -{% block content %} -

    -
    - {{ self.flash() }} -
    - {% block primary_content %}{% endblock %} -
    -{% endblock %} +{% include "home/layout" + (g.homepage_style or '1') + ".html" %} diff --git a/ckan/templates/home/index_base.html b/ckan/templates/home/index_base.html new file mode 100644 index 00000000000..d9e4abfb17b --- /dev/null +++ b/ckan/templates/home/index_base.html @@ -0,0 +1,15 @@ +{% extends "page.html" %} + +{% block subtitle %}{{ _("Welcome") }}{% endblock %} + +{% block maintag %}{% endblock %} +{% block toolbar %}{% endblock %} + +{% block content %} +
    +
    + {{ self.flash() }} +
    + {% block primary_content %}{% endblock %} +
    +{% endblock %} diff --git a/ckanext/homepage/theme/templates/home/layout1.html b/ckan/templates/home/layout1.html similarity index 95% rename from ckanext/homepage/theme/templates/home/layout1.html rename to ckan/templates/home/layout1.html index e40b5fe5e1b..50621df3a1b 100644 --- a/ckanext/homepage/theme/templates/home/layout1.html +++ b/ckan/templates/home/layout1.html @@ -1,4 +1,4 @@ -{% extends 'home/index.html' %} +{% extends 'home/index_base.html' %} {% block homepage_layout_class %} layout-1{% endblock %} diff --git a/ckanext/homepage/theme/templates/home/layout2.html b/ckan/templates/home/layout2.html similarity index 95% rename from ckanext/homepage/theme/templates/home/layout2.html rename to ckan/templates/home/layout2.html index 8ccf432533a..41e625092d2 100644 --- a/ckanext/homepage/theme/templates/home/layout2.html +++ b/ckan/templates/home/layout2.html @@ -1,4 +1,4 @@ -{% extends 'home/index.html' %} +{% extends 'home/index_base.html' %} {% block homepage_layout_class %} layout-2{% endblock %} diff --git a/ckanext/homepage/theme/templates/home/layout3.html b/ckan/templates/home/layout3.html similarity index 93% rename from ckanext/homepage/theme/templates/home/layout3.html rename to ckan/templates/home/layout3.html index 4e805845b21..08e56f45ff3 100644 --- a/ckanext/homepage/theme/templates/home/layout3.html +++ b/ckan/templates/home/layout3.html @@ -1,4 +1,4 @@ -{% extends 'home/index.html' %} +{% extends 'home/index_base.html' %} {% block homepage_layout_class %} layout-3{% endblock %} diff --git a/ckanext/homepage/theme/templates/home/snippets/featured_group.html b/ckan/templates/home/snippets/featured_group.html similarity index 100% rename from ckanext/homepage/theme/templates/home/snippets/featured_group.html rename to ckan/templates/home/snippets/featured_group.html diff --git a/ckanext/homepage/theme/templates/home/snippets/featured_organization.html b/ckan/templates/home/snippets/featured_organization.html similarity index 100% rename from ckanext/homepage/theme/templates/home/snippets/featured_organization.html rename to ckan/templates/home/snippets/featured_organization.html diff --git a/ckanext/homepage/theme/templates/home/snippets/promoted.html b/ckan/templates/home/snippets/promoted.html similarity index 100% rename from ckanext/homepage/theme/templates/home/snippets/promoted.html rename to ckan/templates/home/snippets/promoted.html diff --git a/ckanext/homepage/theme/templates/home/snippets/search.html b/ckan/templates/home/snippets/search.html similarity index 100% rename from ckanext/homepage/theme/templates/home/snippets/search.html rename to ckan/templates/home/snippets/search.html diff --git a/ckanext/homepage/theme/templates/home/snippets/stats.html b/ckan/templates/home/snippets/stats.html similarity index 100% rename from ckanext/homepage/theme/templates/home/snippets/stats.html rename to ckan/templates/home/snippets/stats.html diff --git a/ckanext/homepage/__init__.py b/ckanext/homepage/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/ckanext/homepage/plugin.py b/ckanext/homepage/plugin.py deleted file mode 100644 index d9a79b48a2c..00000000000 --- a/ckanext/homepage/plugin.py +++ /dev/null @@ -1,89 +0,0 @@ -import logging - -import ckan.plugins as p -import ckan.model as model - -log = logging.getLogger(__name__) - -class HomepagePlugin(p.SingletonPlugin): - p.implements(p.IConfigurer, inherit=True) - p.implements(p.ITemplateHelpers, inherit=True) - p.implements(p.IConfigurable, inherit=True) - - def configure(self, config): - groups = config.get('ckan.featured_groups', '') - if groups: - log.warning('Config setting `ckan.featured_groups` is deprecated ' - 'please use `ckanext.homepage.groups`') - self.groups = config.get('ckanext.homepage.groups', groups).split() - self.orgs = config.get('ckanext.homepage.orgs', '').split() - - def update_config(self, config): - p.toolkit.add_template_directory(config, 'theme/templates') - - def get_featured_organizations(self, count=1): - orgs = self.featured_group_org(get_action='organization_show', - list_action='organization_list', - count=count, - items=self.orgs) - return orgs - - def get_featured_groups(self, count=1): - groups = self.featured_group_org(get_action='group_show', - list_action='group_list', - count=count, - items=self.groups) - return groups - - def featured_group_org(self, items, get_action, list_action, count): - def get_group(id): - context = {'ignore_auth': True, - 'limits': {'packages': 2}, - 'for_view': True} - data_dict = {'id': id} - - try: - out = p.toolkit.get_action(get_action)(context, data_dict) - except p.toolkit.ObjectNotFound: - return None - return out - - groups_data = [] - - extras = p.toolkit.get_action(list_action)({}, {}) - - # list of found ids to prevent duplicates - found = [] - for group_name in items + extras: - group = get_group(group_name) - if not group: - continue - # ckeck if duplicate - if group['id'] in found: - continue - found.append(group['id']) - groups_data.append(group) - if len(groups_data) == count: - break - - return groups_data - - def get_site_statistics(self): - stats = {} - stats['dataset_count'] = p.toolkit.get_action('package_search')({}, {"rows": 1})['count'] - stats['group_count'] = len(p.toolkit.get_action('group_list')({}, {})) - stats['organization_count'] = len(p.toolkit.get_action('organization_list')({}, {})) - result =model.Session.execute( - '''select count(*) from related r - left join related_dataset rd on r.id = rd.related_id - where rd.status = 'active' or rd.id is null''').first()[0] - stats['related_count'] = len(p.toolkit.get_action('organization_list')({}, {})) - - return stats - - def get_helpers(self): - return { - 'get_featured_organizations': self.get_featured_organizations, - 'get_featured_groups': self.get_featured_groups, - 'get_site_statistics': self.get_site_statistics, - } diff --git a/doc/configuration.rst b/doc/configuration.rst index 089693d3fbd..3b2b5956153 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -810,6 +810,20 @@ Defines a list of group names or group ids. This setting is used to display groups and datasets from each group on the home page in the default templates (2 groups and 2 datasets for each group are displayed). +.. _ckan.featured_organizations: + +ckan.featured_orgs +^^^^^^^^^^^^^^^^^^^^ + +Example:: + + ckan.featured_orgs = org_one org_two + +Default Value: (empty) + +Defines a list of organization names or ids. This setting is used to display +organization and datasets from each group on the home page in the default +templates (2 groups and 2 datasets for each group are displayed). .. _ckan.gravatar_default: diff --git a/setup.py b/setup.py index 5245b96223d..3f5115bfa8f 100644 --- a/setup.py +++ b/setup.py @@ -129,7 +129,6 @@ [ckan.system_plugins] domain_object_mods = ckan.model.modification:DomainObjectModificationExtension - homepage = ckanext.homepage.plugin:HomepagePlugin [ckan.test_plugins] routes_plugin=tests.ckantestplugins:RoutesPlugin From f1616290b2f96c1c5cb5ad98d3ccdfbc3e9be492 Mon Sep 17 00:00:00 2001 From: John Martin Date: Wed, 2 Oct 2013 11:30:27 +0100 Subject: [PATCH 187/189] [#1126] Removed index_base.html and changed layouts into snippets --- ckan/templates/home/index.html | 19 ++++++++++++- ckan/templates/home/index_base.html | 15 ---------- ckan/templates/home/layout1.html | 42 ++++++++++++--------------- ckan/templates/home/layout2.html | 44 +++++++++++++---------------- ckan/templates/home/layout3.html | 32 +++++++++------------ 5 files changed, 68 insertions(+), 84 deletions(-) delete mode 100644 ckan/templates/home/index_base.html diff --git a/ckan/templates/home/index.html b/ckan/templates/home/index.html index 8684c551732..b28ac82c05d 100644 --- a/ckan/templates/home/index.html +++ b/ckan/templates/home/index.html @@ -1 +1,18 @@ -{% include "home/layout" + (g.homepage_style or '1') + ".html" %} +{% extends "page.html" %} +{% set homepage_style = ( g.homepage_style or '1' ) %} + +{% block subtitle %}{{ _("Welcome") }}{% endblock %} + +{% block maintag %}{% endblock %} +{% block toolbar %}{% endblock %} + +{% block content %} +
    +
    + {{ self.flash() }} +
    + {% block primary_content %} + {% snippet "home/layout{0}.html".format(homepage_style) %} + {% endblock %} +
    +{% endblock %} diff --git a/ckan/templates/home/index_base.html b/ckan/templates/home/index_base.html deleted file mode 100644 index d9e4abfb17b..00000000000 --- a/ckan/templates/home/index_base.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "page.html" %} - -{% block subtitle %}{{ _("Welcome") }}{% endblock %} - -{% block maintag %}{% endblock %} -{% block toolbar %}{% endblock %} - -{% block content %} -
    -
    - {{ self.flash() }} -
    - {% block primary_content %}{% endblock %} -
    -{% endblock %} diff --git a/ckan/templates/home/layout1.html b/ckan/templates/home/layout1.html index 50621df3a1b..53ac0d390bb 100644 --- a/ckan/templates/home/layout1.html +++ b/ckan/templates/home/layout1.html @@ -1,30 +1,24 @@ -{% extends 'home/index_base.html' %} - -{% block homepage_layout_class %} layout-1{% endblock %} - -{% block primary_content %} -
    -
    -
    -
    - {% snippet 'home/snippets/promoted.html' %} -
    -
    - {% snippet 'home/snippets/search.html' %} -
    +
    +
    +
    +
    + {% snippet 'home/snippets/promoted.html' %} +
    +
    + {% snippet 'home/snippets/search.html' %}
    -
    -
    -
    -
    - {% snippet 'home/snippets/featured_group.html' %} -
    -
    - {% snippet 'home/snippets/featured_organization.html' %} -
    +
    +
    +
    +
    +
    + {% snippet 'home/snippets/featured_group.html' %} +
    +
    + {% snippet 'home/snippets/featured_organization.html' %}
    -{% endblock %} +
    diff --git a/ckan/templates/home/layout2.html b/ckan/templates/home/layout2.html index 41e625092d2..e1b62bea09f 100644 --- a/ckan/templates/home/layout2.html +++ b/ckan/templates/home/layout2.html @@ -1,31 +1,25 @@ -{% extends 'home/index_base.html' %} - -{% block homepage_layout_class %} layout-2{% endblock %} - -{% block primary_content %} -
    -
    -
    -
    - {% snippet 'home/snippets/search.html' %} - {% snippet 'home/snippets/stats.html' %} -
    -
    - {% snippet 'home/snippets/promoted.html' %} -
    +
    +
    +
    +
    + {% snippet 'home/snippets/search.html' %} + {% snippet 'home/snippets/stats.html' %} +
    +
    + {% snippet 'home/snippets/promoted.html' %}
    -
    -
    -
    -
    - {% snippet 'home/snippets/featured_organization.html' %} -
    -
    - {% snippet 'home/snippets/featured_group.html' %} -
    +
    +
    +
    +
    +
    + {% snippet 'home/snippets/featured_organization.html' %} +
    +
    + {% snippet 'home/snippets/featured_group.html' %}
    -{% endblock %} +
    diff --git a/ckan/templates/home/layout3.html b/ckan/templates/home/layout3.html index 08e56f45ff3..80c10c22ba0 100644 --- a/ckan/templates/home/layout3.html +++ b/ckan/templates/home/layout3.html @@ -1,23 +1,17 @@ -{% extends 'home/index_base.html' %} - -{% block homepage_layout_class %} layout-3{% endblock %} - -{% block primary_content %} -
    -
    - {% snippet 'home/snippets/search.html' %} -
    +
    +
    + {% snippet 'home/snippets/search.html' %}
    -
    -
    -
    -
    - {% snippet 'home/snippets/promoted.html' %} -
    -
    - {% snippet 'home/snippets/stats.html' %} -
    +
    +
    +
    +
    +
    + {% snippet 'home/snippets/promoted.html' %} +
    +
    + {% snippet 'home/snippets/stats.html' %}
    -{% endblock %} +
    From 4ee4ddb84f6c4f34adfa9783e7ee531c9664a8b3 Mon Sep 17 00:00:00 2001 From: John Glover Date: Thu, 3 Oct 2013 10:31:42 +0200 Subject: [PATCH 188/189] [#1126] PEP8 and fix typo --- ckan/lib/helpers.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py index 94e7c97b0e6..edd9746f0a8 100644 --- a/ckan/lib/helpers.py +++ b/ckan/lib/helpers.py @@ -1663,6 +1663,7 @@ def new_activities(): action = logic.get_action('dashboard_new_activities_count') return action({}, {}) + def get_featured_organizations(count=1): '''Returns a list of favourite organization in the form of organization_list action function @@ -1674,6 +1675,7 @@ def get_featured_organizations(count=1): items=config_orgs) return orgs + def get_featured_groups(count=1): '''Returns a list of favourite group the form of organization_list action function @@ -1685,6 +1687,7 @@ def get_featured_groups(count=1): items=config_groups) return groups + def featured_group_org(items, get_action, list_action, count): def get_group(id): context = {'ignore_auth': True, @@ -1708,7 +1711,7 @@ def get_group(id): group = get_group(group_name) if not group: continue - # ckeck if duplicate + # check if duplicate if group['id'] in found: continue found.append(group['id']) @@ -1718,12 +1721,15 @@ def get_group(id): return groups_data + def get_site_statistics(): stats = {} - stats['dataset_count'] = logic.get_action('package_search')({}, {"rows": 1})['count'] + stats['dataset_count'] = logic.get_action('package_search')( + {}, {"rows": 1})['count'] stats['group_count'] = len(logic.get_action('group_list')({}, {})) - stats['organization_count'] = len(logic.get_action('organization_list')({}, {})) - result =model.Session.execute( + stats['organization_count'] = len( + logic.get_action('organization_list')({}, {})) + result = model.Session.execute( '''select count(*) from related r left join related_dataset rd on r.id = rd.related_id where rd.status = 'active' or rd.id is null''').first()[0] From ae5a51c6d1765bf2d8271fa4c67a4a79ed5d8451 Mon Sep 17 00:00:00 2001 From: John Glover Date: Thu, 3 Oct 2013 10:51:58 +0200 Subject: [PATCH 189/189] [#1126] Update css after merge --- ckan/public/base/css/fuchsia.css | 971 +++---------------------------- ckan/public/base/css/green.css | 971 +++---------------------------- ckan/public/base/css/main.css | 122 ++-- ckan/public/base/css/maroon.css | 971 +++---------------------------- ckan/public/base/css/red.css | 971 +++---------------------------- 5 files changed, 374 insertions(+), 3632 deletions(-) diff --git a/ckan/public/base/css/fuchsia.css b/ckan/public/base/css/fuchsia.css index 300d2981817..a8a33f21105 100644 --- a/ckan/public/base/css/fuchsia.css +++ b/ckan/public/base/css/fuchsia.css @@ -2055,6 +2055,14 @@ table th[class*="span"], .open > .dropdown-menu { display: block; } +.dropdown-backdrop { + position: fixed; + left: 0; + right: 0; + bottom: 0; + top: 0; + z-index: 990; +} .pull-right > .dropdown-menu { right: 0; left: auto; @@ -5572,6 +5580,12 @@ textarea { .control-large input { height: 41px; } +.control-required { + color: #c6898b; +} +.form-actions .control-required-message { + float: left; +} .form-actions { background: none; margin-left: -25px; @@ -6904,842 +6918,6 @@ h4 small { height: 35px; background-position: -320px -62px; } -/* This is a modified version of the font-awesome core less document. */ -@font-face { - font-family: 'FontAwesome'; - src: url('../../../base/vendor/font-awesome/font/fontawesome-webfont.eot?v=3.0.1'); - src: url('../../../base/vendor/font-awesome/font/fontawesome-webfont.eot?#iefix&v=3.0.1') format('embedded-opentype'), url('../../../base/vendor/font-awesome/font/fontawesome-webfont.woff?v=3.0.1') format('woff'), url('../../../base/vendor/font-awesome/font/fontawesome-webfont.ttf?v=3.0.1') format('truetype'); - font-weight: normal; - font-style: normal; -} -[class^="icon-"], -[class*=" icon-"] { - font-family: FontAwesome; - font-weight: normal; - font-style: normal; - text-decoration: inherit; - -webkit-font-smoothing: antialiased; - display: inline; - width: auto; - height: auto; - line-height: normal; - vertical-align: baseline; - background-image: none; - background-position: 0% 0%; - background-repeat: repeat; - margin-top: 0; -} -.icon-spin { - display: inline-block; - -moz-animation: spin 2s infinite linear; - -o-animation: spin 2s infinite linear; - -webkit-animation: spin 2s infinite linear; - animation: spin 2s infinite linear; -} -@-moz-keyframes spin { - 0% { - -moz-transform: rotate(0deg); - } - 100% { - -moz-transform: rotate(359deg); - } -} -@-webkit-keyframes spin { - 0% { - -webkit-transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - } -} -@-o-keyframes spin { - 0% { - -o-transform: rotate(0deg); - } - 100% { - -o-transform: rotate(359deg); - } -} -@-ms-keyframes spin { - 0% { - -ms-transform: rotate(0deg); - } - 100% { - -ms-transform: rotate(359deg); - } -} -@keyframes spin { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(359deg); - } -} -@-moz-document url-prefix() { - .icon-spin { - height: .9em; - } - .btn .icon-spin { - height: auto; - } - .icon-spin.icon-large { - height: 1.25em; - } - .btn .icon-spin.icon-large { - height: .75em; - } -} -/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen - readers do not read off random characters that represent icons */ -.icon-glass:before { - content: "\f000"; -} -.icon-music:before { - content: "\f001"; -} -.icon-search:before { - content: "\f002"; -} -.icon-envelope:before { - content: "\f003"; -} -.icon-heart:before { - content: "\f004"; -} -.icon-star:before { - content: "\f005"; -} -.icon-star-empty:before { - content: "\f006"; -} -.icon-user:before { - content: "\f007"; -} -.icon-film:before { - content: "\f008"; -} -.icon-th-large:before { - content: "\f009"; -} -.icon-th:before { - content: "\f00a"; -} -.icon-th-list:before { - content: "\f00b"; -} -.icon-ok:before { - content: "\f00c"; -} -.icon-remove:before { - content: "\f00d"; -} -.icon-zoom-in:before { - content: "\f00e"; -} -.icon-zoom-out:before { - content: "\f010"; -} -.icon-off:before { - content: "\f011"; -} -.icon-signal:before { - content: "\f012"; -} -.icon-cog:before { - content: "\f013"; -} -.icon-trash:before { - content: "\f014"; -} -.icon-home:before { - content: "\f015"; -} -.icon-file:before { - content: "\f016"; -} -.icon-time:before { - content: "\f017"; -} -.icon-road:before { - content: "\f018"; -} -.icon-download-alt:before { - content: "\f019"; -} -.icon-download:before { - content: "\f01a"; -} -.icon-upload:before { - content: "\f01b"; -} -.icon-inbox:before { - content: "\f01c"; -} -.icon-play-circle:before { - content: "\f01d"; -} -.icon-repeat:before { - content: "\f01e"; -} -/* \f020 doesn't work in Safari. all shifted one down */ -.icon-refresh:before { - content: "\f021"; -} -.icon-list-alt:before { - content: "\f022"; -} -.icon-lock:before { - content: "\f023"; -} -.icon-flag:before { - content: "\f024"; -} -.icon-headphones:before { - content: "\f025"; -} -.icon-volume-off:before { - content: "\f026"; -} -.icon-volume-down:before { - content: "\f027"; -} -.icon-volume-up:before { - content: "\f028"; -} -.icon-qrcode:before { - content: "\f029"; -} -.icon-barcode:before { - content: "\f02a"; -} -.icon-tag:before { - content: "\f02b"; -} -.icon-tags:before { - content: "\f02c"; -} -.icon-book:before { - content: "\f02d"; -} -.icon-bookmark:before { - content: "\f02e"; -} -.icon-print:before { - content: "\f02f"; -} -.icon-camera:before { - content: "\f030"; -} -.icon-font:before { - content: "\f031"; -} -.icon-bold:before { - content: "\f032"; -} -.icon-italic:before { - content: "\f033"; -} -.icon-text-height:before { - content: "\f034"; -} -.icon-text-width:before { - content: "\f035"; -} -.icon-align-left:before { - content: "\f036"; -} -.icon-align-center:before { - content: "\f037"; -} -.icon-align-right:before { - content: "\f038"; -} -.icon-align-justify:before { - content: "\f039"; -} -.icon-list:before { - content: "\f03a"; -} -.icon-indent-left:before { - content: "\f03b"; -} -.icon-indent-right:before { - content: "\f03c"; -} -.icon-facetime-video:before { - content: "\f03d"; -} -.icon-picture:before { - content: "\f03e"; -} -.icon-pencil:before { - content: "\f040"; -} -.icon-map-marker:before { - content: "\f041"; -} -.icon-adjust:before { - content: "\f042"; -} -.icon-tint:before { - content: "\f043"; -} -.icon-edit:before { - content: "\f044"; -} -.icon-share:before { - content: "\f045"; -} -.icon-check:before { - content: "\f046"; -} -.icon-move:before { - content: "\f047"; -} -.icon-step-backward:before { - content: "\f048"; -} -.icon-fast-backward:before { - content: "\f049"; -} -.icon-backward:before { - content: "\f04a"; -} -.icon-play:before { - content: "\f04b"; -} -.icon-pause:before { - content: "\f04c"; -} -.icon-stop:before { - content: "\f04d"; -} -.icon-forward:before { - content: "\f04e"; -} -.icon-fast-forward:before { - content: "\f050"; -} -.icon-step-forward:before { - content: "\f051"; -} -.icon-eject:before { - content: "\f052"; -} -.icon-chevron-left:before { - content: "\f053"; -} -.icon-chevron-right:before { - content: "\f054"; -} -.icon-plus-sign:before { - content: "\f055"; -} -.icon-minus-sign:before { - content: "\f056"; -} -.icon-remove-sign:before { - content: "\f057"; -} -.icon-ok-sign:before { - content: "\f058"; -} -.icon-question-sign:before { - content: "\f059"; -} -.icon-info-sign:before { - content: "\f05a"; -} -.icon-screenshot:before { - content: "\f05b"; -} -.icon-remove-circle:before { - content: "\f05c"; -} -.icon-ok-circle:before { - content: "\f05d"; -} -.icon-ban-circle:before { - content: "\f05e"; -} -.icon-arrow-left:before { - content: "\f060"; -} -.icon-arrow-right:before { - content: "\f061"; -} -.icon-arrow-up:before { - content: "\f062"; -} -.icon-arrow-down:before { - content: "\f063"; -} -.icon-share-alt:before { - content: "\f064"; -} -.icon-resize-full:before { - content: "\f065"; -} -.icon-resize-small:before { - content: "\f066"; -} -.icon-plus:before { - content: "\f067"; -} -.icon-minus:before { - content: "\f068"; -} -.icon-asterisk:before { - content: "\f069"; -} -.icon-exclamation-sign:before { - content: "\f06a"; -} -.icon-gift:before { - content: "\f06b"; -} -.icon-leaf:before { - content: "\f06c"; -} -.icon-fire:before { - content: "\f06d"; -} -.icon-eye-open:before { - content: "\f06e"; -} -.icon-eye-close:before { - content: "\f070"; -} -.icon-warning-sign:before { - content: "\f071"; -} -.icon-plane:before { - content: "\f072"; -} -.icon-calendar:before { - content: "\f073"; -} -.icon-random:before { - content: "\f074"; -} -.icon-comment:before { - content: "\f075"; -} -.icon-magnet:before { - content: "\f076"; -} -.icon-chevron-up:before { - content: "\f077"; -} -.icon-chevron-down:before { - content: "\f078"; -} -.icon-retweet:before { - content: "\f079"; -} -.icon-shopping-cart:before { - content: "\f07a"; -} -.icon-folder-close:before { - content: "\f07b"; -} -.icon-folder-open:before { - content: "\f07c"; -} -.icon-resize-vertical:before { - content: "\f07d"; -} -.icon-resize-horizontal:before { - content: "\f07e"; -} -.icon-bar-chart:before { - content: "\f080"; -} -.icon-twitter-sign:before { - content: "\f081"; -} -.icon-facebook-sign:before { - content: "\f082"; -} -.icon-camera-retro:before { - content: "\f083"; -} -.icon-key:before { - content: "\f084"; -} -.icon-cogs:before { - content: "\f085"; -} -.icon-comments:before { - content: "\f086"; -} -.icon-thumbs-up:before { - content: "\f087"; -} -.icon-thumbs-down:before { - content: "\f088"; -} -.icon-star-half:before { - content: "\f089"; -} -.icon-heart-empty:before { - content: "\f08a"; -} -.icon-signout:before { - content: "\f08b"; -} -.icon-linkedin-sign:before { - content: "\f08c"; -} -.icon-pushpin:before { - content: "\f08d"; -} -.icon-external-link:before { - content: "\f08e"; -} -.icon-signin:before { - content: "\f090"; -} -.icon-trophy:before { - content: "\f091"; -} -.icon-github-sign:before { - content: "\f092"; -} -.icon-upload-alt:before { - content: "\f093"; -} -.icon-lemon:before { - content: "\f094"; -} -.icon-phone:before { - content: "\f095"; -} -.icon-check-empty:before { - content: "\f096"; -} -.icon-bookmark-empty:before { - content: "\f097"; -} -.icon-phone-sign:before { - content: "\f098"; -} -.icon-twitter:before { - content: "\f099"; -} -.icon-facebook:before { - content: "\f09a"; -} -.icon-github:before { - content: "\f09b"; -} -.icon-unlock:before { - content: "\f09c"; -} -.icon-credit-card:before { - content: "\f09d"; -} -.icon-rss:before { - content: "\f09e"; -} -.icon-hdd:before { - content: "\f0a0"; -} -.icon-bullhorn:before { - content: "\f0a1"; -} -.icon-bell:before { - content: "\f0a2"; -} -.icon-certificate:before { - content: "\f0a3"; -} -.icon-hand-right:before { - content: "\f0a4"; -} -.icon-hand-left:before { - content: "\f0a5"; -} -.icon-hand-up:before { - content: "\f0a6"; -} -.icon-hand-down:before { - content: "\f0a7"; -} -.icon-circle-arrow-left:before { - content: "\f0a8"; -} -.icon-circle-arrow-right:before { - content: "\f0a9"; -} -.icon-circle-arrow-up:before { - content: "\f0aa"; -} -.icon-circle-arrow-down:before { - content: "\f0ab"; -} -.icon-globe:before { - content: "\f0ac"; -} -.icon-wrench:before { - content: "\f0ad"; -} -.icon-tasks:before { - content: "\f0ae"; -} -.icon-filter:before { - content: "\f0b0"; -} -.icon-briefcase:before { - content: "\f0b1"; -} -.icon-fullscreen:before { - content: "\f0b2"; -} -.icon-group:before { - content: "\f0c0"; -} -.icon-link:before { - content: "\f0c1"; -} -.icon-cloud:before { - content: "\f0c2"; -} -.icon-beaker:before { - content: "\f0c3"; -} -.icon-cut:before { - content: "\f0c4"; -} -.icon-copy:before { - content: "\f0c5"; -} -.icon-paper-clip:before { - content: "\f0c6"; -} -.icon-save:before { - content: "\f0c7"; -} -.icon-sign-blank:before { - content: "\f0c8"; -} -.icon-reorder:before { - content: "\f0c9"; -} -.icon-list-ul:before { - content: "\f0ca"; -} -.icon-list-ol:before { - content: "\f0cb"; -} -.icon-strikethrough:before { - content: "\f0cc"; -} -.icon-underline:before { - content: "\f0cd"; -} -.icon-table:before { - content: "\f0ce"; -} -.icon-magic:before { - content: "\f0d0"; -} -.icon-truck:before { - content: "\f0d1"; -} -.icon-pinterest:before { - content: "\f0d2"; -} -.icon-pinterest-sign:before { - content: "\f0d3"; -} -.icon-google-plus-sign:before { - content: "\f0d4"; -} -.icon-google-plus:before { - content: "\f0d5"; -} -.icon-money:before { - content: "\f0d6"; -} -.icon-caret-down:before { - content: "\f0d7"; -} -.icon-caret-up:before { - content: "\f0d8"; -} -.icon-caret-left:before { - content: "\f0d9"; -} -.icon-caret-right:before { - content: "\f0da"; -} -.icon-columns:before { - content: "\f0db"; -} -.icon-sort:before { - content: "\f0dc"; -} -.icon-sort-down:before { - content: "\f0dd"; -} -.icon-sort-up:before { - content: "\f0de"; -} -.icon-envelope-alt:before { - content: "\f0e0"; -} -.icon-linkedin:before { - content: "\f0e1"; -} -.icon-undo:before { - content: "\f0e2"; -} -.icon-legal:before { - content: "\f0e3"; -} -.icon-dashboard:before { - content: "\f0e4"; -} -.icon-comment-alt:before { - content: "\f0e5"; -} -.icon-comments-alt:before { - content: "\f0e6"; -} -.icon-bolt:before { - content: "\f0e7"; -} -.icon-sitemap:before { - content: "\f0e8"; -} -.icon-umbrella:before { - content: "\f0e9"; -} -.icon-paste:before { - content: "\f0ea"; -} -.icon-lightbulb:before { - content: "\f0eb"; -} -.icon-exchange:before { - content: "\f0ec"; -} -.icon-cloud-download:before { - content: "\f0ed"; -} -.icon-cloud-upload:before { - content: "\f0ee"; -} -.icon-user-md:before { - content: "\f0f0"; -} -.icon-stethoscope:before { - content: "\f0f1"; -} -.icon-suitcase:before { - content: "\f0f2"; -} -.icon-bell-alt:before { - content: "\f0f3"; -} -.icon-coffee:before { - content: "\f0f4"; -} -.icon-food:before { - content: "\f0f5"; -} -.icon-file-alt:before { - content: "\f0f6"; -} -.icon-building:before { - content: "\f0f7"; -} -.icon-hospital:before { - content: "\f0f8"; -} -.icon-ambulance:before { - content: "\f0f9"; -} -.icon-medkit:before { - content: "\f0fa"; -} -.icon-fighter-jet:before { - content: "\f0fb"; -} -.icon-beer:before { - content: "\f0fc"; -} -.icon-h-sign:before { - content: "\f0fd"; -} -.icon-plus-sign-alt:before { - content: "\f0fe"; -} -.icon-double-angle-left:before { - content: "\f100"; -} -.icon-double-angle-right:before { - content: "\f101"; -} -.icon-double-angle-up:before { - content: "\f102"; -} -.icon-double-angle-down:before { - content: "\f103"; -} -.icon-angle-left:before { - content: "\f104"; -} -.icon-angle-right:before { - content: "\f105"; -} -.icon-angle-up:before { - content: "\f106"; -} -.icon-angle-down:before { - content: "\f107"; -} -.icon-desktop:before { - content: "\f108"; -} -.icon-laptop:before { - content: "\f109"; -} -.icon-tablet:before { - content: "\f10a"; -} -.icon-mobile-phone:before { - content: "\f10b"; -} -.icon-circle-blank:before { - content: "\f10c"; -} -.icon-quote-left:before { - content: "\f10d"; -} -.icon-quote-right:before { - content: "\f10e"; -} -.icon-spinner:before { - content: "\f110"; -} -.icon-circle:before { - content: "\f111"; -} -.icon-reply:before { - content: "\f112"; -} -.icon-github-alt:before { - content: "\f113"; -} -.icon-folder-close-alt:before { - content: "\f114"; -} -.icon-folder-open-alt:before { - content: "\f115"; -} [class^="icon-"], [class*=" icon-"] { display: inline-block; @@ -7963,45 +7141,39 @@ h4 small { .context-info.editing .module-content { margin-top: 0; } -.hero { - background: url("../../../base/images/background-tile.png"); +.homepage [role=main] { padding: 20px 0; min-height: 0; } -.hero > .container { +.homepage .row { position: relative; - padding-bottom: 0; } -.hero .search-giant { - margin-bottom: 10px; -} -.hero .search-giant input { - border-color: #003f52; -} -.hero .page-heading { - font-size: 18px; - margin-bottom: 0; -} -.hero .module-dark { +.homepage .module-search { padding: 5px; - margin-bottom: 0; + margin: 0; color: #ffffff; background: #ffffff; } -.hero .module-dark .module-content { +.homepage .module-search .search-giant { + margin-bottom: 10px; +} +.homepage .module-search .search-giant input { + border-color: #003f52; +} +.homepage .module-search .module-content { -webkit-border-radius: 3px 3px 0 0; -moz-border-radius: 3px 3px 0 0; border-radius: 3px 3px 0 0; background-color: #e73892; border-bottom: none; } -.hero .module-dark .module-content .heading { +.homepage .module-search .module-content .heading { margin-top: 0; margin-bottom: 7px; font-size: 24px; line-height: 40px; } -.hero .tags { +.homepage .module-search .tags { *zoom: 1; padding: 5px 10px 10px 10px; background-color: #d31979; @@ -8009,69 +7181,77 @@ h4 small { -moz-border-radius: 0 0 3px 3px; border-radius: 0 0 3px 3px; } -.hero .tags:before, -.hero .tags:after { +.homepage .module-search .tags:before, +.homepage .module-search .tags:after { display: table; content: ""; line-height: 0; } -.hero .tags:after { +.homepage .module-search .tags:after { clear: both; } -.hero .tags h3, -.hero .tags .tag { +.homepage .module-search .tags h3, +.homepage .module-search .tags .tag { display: block; float: left; margin: 5px 10px 0 0; } -.hero .tags h3 { +.homepage .module-search .tags h3 { font-size: 14px; line-height: 20px; padding: 2px 8px; } -.hero-primary, -.hero-secondary { +.homepage .group-list { + margin: 0; +} +.homepage .box .inner { + padding: 20px 25px; +} +.homepage .stats h3 { + margin: 0 0 10px 0; +} +.homepage .stats ul { + margin: 0; + list-style: none; + *zoom: 1; +} +.homepage .stats ul:before, +.homepage .stats ul:after { + display: table; + content: ""; + line-height: 0; +} +.homepage .stats ul:after { + clear: both; +} +.homepage .stats ul li { float: left; - margin-left: 20px; - width: 460px; + width: 25%; + font-weight: 300; } -.hero-primary { - margin-left: 0; - margin-bottom: 0; +.homepage .stats ul li a { + display: block; } -.hero-secondary { +.homepage .stats ul li a b { + display: block; + font-size: 35px; + line-height: 1.5; +} +.homepage .stats ul li a:hover { + text-decoration: none; +} +.homepage.layout-1 .row1 .col2 { position: absolute; bottom: 0; right: 0; } -.hero-secondary .hero-secondary-inner { +.homepage.layout-1 .row1 .col2 .module-search { bottom: 0; left: 0; right: 0; } -.main.homepage { - padding-top: 20px; - padding-bottom: 20px; - border-top: 1px solid #cccccc; -} -.main.homepage .module-heading .media-image { - margin-right: 15px; - max-height: 53px; - -webkit-box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5); - -moz-box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5); - box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5); -} -.main.homepage .group-listing .box { - min-height: 275px; -} -.main.homepage .group-list { - margin-bottom: 0; -} -.main.homepage .group-list .dataset-content { - min-height: 70px; -} -.main.homepage .box .module { - margin-top: 0; +.homepage.layout-2 .stats { + margin-top: 20px; } .account-masthead { *zoom: 1; @@ -8920,6 +8100,11 @@ iframe { padding-bottom: 20px !important; margin-bottom: 0 !important; } +.ie8 .lang-dropdown, +.ie7 .lang-dropdown { + position: relative !important; + top: -90px !important; +} .ie7 .alert { position: relative; } diff --git a/ckan/public/base/css/green.css b/ckan/public/base/css/green.css index 1cc58fa82dc..83bbcd5eefa 100644 --- a/ckan/public/base/css/green.css +++ b/ckan/public/base/css/green.css @@ -2055,6 +2055,14 @@ table th[class*="span"], .open > .dropdown-menu { display: block; } +.dropdown-backdrop { + position: fixed; + left: 0; + right: 0; + bottom: 0; + top: 0; + z-index: 990; +} .pull-right > .dropdown-menu { right: 0; left: auto; @@ -5572,6 +5580,12 @@ textarea { .control-large input { height: 41px; } +.control-required { + color: #c6898b; +} +.form-actions .control-required-message { + float: left; +} .form-actions { background: none; margin-left: -25px; @@ -6904,842 +6918,6 @@ h4 small { height: 35px; background-position: -320px -62px; } -/* This is a modified version of the font-awesome core less document. */ -@font-face { - font-family: 'FontAwesome'; - src: url('../../../base/vendor/font-awesome/font/fontawesome-webfont.eot?v=3.0.1'); - src: url('../../../base/vendor/font-awesome/font/fontawesome-webfont.eot?#iefix&v=3.0.1') format('embedded-opentype'), url('../../../base/vendor/font-awesome/font/fontawesome-webfont.woff?v=3.0.1') format('woff'), url('../../../base/vendor/font-awesome/font/fontawesome-webfont.ttf?v=3.0.1') format('truetype'); - font-weight: normal; - font-style: normal; -} -[class^="icon-"], -[class*=" icon-"] { - font-family: FontAwesome; - font-weight: normal; - font-style: normal; - text-decoration: inherit; - -webkit-font-smoothing: antialiased; - display: inline; - width: auto; - height: auto; - line-height: normal; - vertical-align: baseline; - background-image: none; - background-position: 0% 0%; - background-repeat: repeat; - margin-top: 0; -} -.icon-spin { - display: inline-block; - -moz-animation: spin 2s infinite linear; - -o-animation: spin 2s infinite linear; - -webkit-animation: spin 2s infinite linear; - animation: spin 2s infinite linear; -} -@-moz-keyframes spin { - 0% { - -moz-transform: rotate(0deg); - } - 100% { - -moz-transform: rotate(359deg); - } -} -@-webkit-keyframes spin { - 0% { - -webkit-transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - } -} -@-o-keyframes spin { - 0% { - -o-transform: rotate(0deg); - } - 100% { - -o-transform: rotate(359deg); - } -} -@-ms-keyframes spin { - 0% { - -ms-transform: rotate(0deg); - } - 100% { - -ms-transform: rotate(359deg); - } -} -@keyframes spin { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(359deg); - } -} -@-moz-document url-prefix() { - .icon-spin { - height: .9em; - } - .btn .icon-spin { - height: auto; - } - .icon-spin.icon-large { - height: 1.25em; - } - .btn .icon-spin.icon-large { - height: .75em; - } -} -/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen - readers do not read off random characters that represent icons */ -.icon-glass:before { - content: "\f000"; -} -.icon-music:before { - content: "\f001"; -} -.icon-search:before { - content: "\f002"; -} -.icon-envelope:before { - content: "\f003"; -} -.icon-heart:before { - content: "\f004"; -} -.icon-star:before { - content: "\f005"; -} -.icon-star-empty:before { - content: "\f006"; -} -.icon-user:before { - content: "\f007"; -} -.icon-film:before { - content: "\f008"; -} -.icon-th-large:before { - content: "\f009"; -} -.icon-th:before { - content: "\f00a"; -} -.icon-th-list:before { - content: "\f00b"; -} -.icon-ok:before { - content: "\f00c"; -} -.icon-remove:before { - content: "\f00d"; -} -.icon-zoom-in:before { - content: "\f00e"; -} -.icon-zoom-out:before { - content: "\f010"; -} -.icon-off:before { - content: "\f011"; -} -.icon-signal:before { - content: "\f012"; -} -.icon-cog:before { - content: "\f013"; -} -.icon-trash:before { - content: "\f014"; -} -.icon-home:before { - content: "\f015"; -} -.icon-file:before { - content: "\f016"; -} -.icon-time:before { - content: "\f017"; -} -.icon-road:before { - content: "\f018"; -} -.icon-download-alt:before { - content: "\f019"; -} -.icon-download:before { - content: "\f01a"; -} -.icon-upload:before { - content: "\f01b"; -} -.icon-inbox:before { - content: "\f01c"; -} -.icon-play-circle:before { - content: "\f01d"; -} -.icon-repeat:before { - content: "\f01e"; -} -/* \f020 doesn't work in Safari. all shifted one down */ -.icon-refresh:before { - content: "\f021"; -} -.icon-list-alt:before { - content: "\f022"; -} -.icon-lock:before { - content: "\f023"; -} -.icon-flag:before { - content: "\f024"; -} -.icon-headphones:before { - content: "\f025"; -} -.icon-volume-off:before { - content: "\f026"; -} -.icon-volume-down:before { - content: "\f027"; -} -.icon-volume-up:before { - content: "\f028"; -} -.icon-qrcode:before { - content: "\f029"; -} -.icon-barcode:before { - content: "\f02a"; -} -.icon-tag:before { - content: "\f02b"; -} -.icon-tags:before { - content: "\f02c"; -} -.icon-book:before { - content: "\f02d"; -} -.icon-bookmark:before { - content: "\f02e"; -} -.icon-print:before { - content: "\f02f"; -} -.icon-camera:before { - content: "\f030"; -} -.icon-font:before { - content: "\f031"; -} -.icon-bold:before { - content: "\f032"; -} -.icon-italic:before { - content: "\f033"; -} -.icon-text-height:before { - content: "\f034"; -} -.icon-text-width:before { - content: "\f035"; -} -.icon-align-left:before { - content: "\f036"; -} -.icon-align-center:before { - content: "\f037"; -} -.icon-align-right:before { - content: "\f038"; -} -.icon-align-justify:before { - content: "\f039"; -} -.icon-list:before { - content: "\f03a"; -} -.icon-indent-left:before { - content: "\f03b"; -} -.icon-indent-right:before { - content: "\f03c"; -} -.icon-facetime-video:before { - content: "\f03d"; -} -.icon-picture:before { - content: "\f03e"; -} -.icon-pencil:before { - content: "\f040"; -} -.icon-map-marker:before { - content: "\f041"; -} -.icon-adjust:before { - content: "\f042"; -} -.icon-tint:before { - content: "\f043"; -} -.icon-edit:before { - content: "\f044"; -} -.icon-share:before { - content: "\f045"; -} -.icon-check:before { - content: "\f046"; -} -.icon-move:before { - content: "\f047"; -} -.icon-step-backward:before { - content: "\f048"; -} -.icon-fast-backward:before { - content: "\f049"; -} -.icon-backward:before { - content: "\f04a"; -} -.icon-play:before { - content: "\f04b"; -} -.icon-pause:before { - content: "\f04c"; -} -.icon-stop:before { - content: "\f04d"; -} -.icon-forward:before { - content: "\f04e"; -} -.icon-fast-forward:before { - content: "\f050"; -} -.icon-step-forward:before { - content: "\f051"; -} -.icon-eject:before { - content: "\f052"; -} -.icon-chevron-left:before { - content: "\f053"; -} -.icon-chevron-right:before { - content: "\f054"; -} -.icon-plus-sign:before { - content: "\f055"; -} -.icon-minus-sign:before { - content: "\f056"; -} -.icon-remove-sign:before { - content: "\f057"; -} -.icon-ok-sign:before { - content: "\f058"; -} -.icon-question-sign:before { - content: "\f059"; -} -.icon-info-sign:before { - content: "\f05a"; -} -.icon-screenshot:before { - content: "\f05b"; -} -.icon-remove-circle:before { - content: "\f05c"; -} -.icon-ok-circle:before { - content: "\f05d"; -} -.icon-ban-circle:before { - content: "\f05e"; -} -.icon-arrow-left:before { - content: "\f060"; -} -.icon-arrow-right:before { - content: "\f061"; -} -.icon-arrow-up:before { - content: "\f062"; -} -.icon-arrow-down:before { - content: "\f063"; -} -.icon-share-alt:before { - content: "\f064"; -} -.icon-resize-full:before { - content: "\f065"; -} -.icon-resize-small:before { - content: "\f066"; -} -.icon-plus:before { - content: "\f067"; -} -.icon-minus:before { - content: "\f068"; -} -.icon-asterisk:before { - content: "\f069"; -} -.icon-exclamation-sign:before { - content: "\f06a"; -} -.icon-gift:before { - content: "\f06b"; -} -.icon-leaf:before { - content: "\f06c"; -} -.icon-fire:before { - content: "\f06d"; -} -.icon-eye-open:before { - content: "\f06e"; -} -.icon-eye-close:before { - content: "\f070"; -} -.icon-warning-sign:before { - content: "\f071"; -} -.icon-plane:before { - content: "\f072"; -} -.icon-calendar:before { - content: "\f073"; -} -.icon-random:before { - content: "\f074"; -} -.icon-comment:before { - content: "\f075"; -} -.icon-magnet:before { - content: "\f076"; -} -.icon-chevron-up:before { - content: "\f077"; -} -.icon-chevron-down:before { - content: "\f078"; -} -.icon-retweet:before { - content: "\f079"; -} -.icon-shopping-cart:before { - content: "\f07a"; -} -.icon-folder-close:before { - content: "\f07b"; -} -.icon-folder-open:before { - content: "\f07c"; -} -.icon-resize-vertical:before { - content: "\f07d"; -} -.icon-resize-horizontal:before { - content: "\f07e"; -} -.icon-bar-chart:before { - content: "\f080"; -} -.icon-twitter-sign:before { - content: "\f081"; -} -.icon-facebook-sign:before { - content: "\f082"; -} -.icon-camera-retro:before { - content: "\f083"; -} -.icon-key:before { - content: "\f084"; -} -.icon-cogs:before { - content: "\f085"; -} -.icon-comments:before { - content: "\f086"; -} -.icon-thumbs-up:before { - content: "\f087"; -} -.icon-thumbs-down:before { - content: "\f088"; -} -.icon-star-half:before { - content: "\f089"; -} -.icon-heart-empty:before { - content: "\f08a"; -} -.icon-signout:before { - content: "\f08b"; -} -.icon-linkedin-sign:before { - content: "\f08c"; -} -.icon-pushpin:before { - content: "\f08d"; -} -.icon-external-link:before { - content: "\f08e"; -} -.icon-signin:before { - content: "\f090"; -} -.icon-trophy:before { - content: "\f091"; -} -.icon-github-sign:before { - content: "\f092"; -} -.icon-upload-alt:before { - content: "\f093"; -} -.icon-lemon:before { - content: "\f094"; -} -.icon-phone:before { - content: "\f095"; -} -.icon-check-empty:before { - content: "\f096"; -} -.icon-bookmark-empty:before { - content: "\f097"; -} -.icon-phone-sign:before { - content: "\f098"; -} -.icon-twitter:before { - content: "\f099"; -} -.icon-facebook:before { - content: "\f09a"; -} -.icon-github:before { - content: "\f09b"; -} -.icon-unlock:before { - content: "\f09c"; -} -.icon-credit-card:before { - content: "\f09d"; -} -.icon-rss:before { - content: "\f09e"; -} -.icon-hdd:before { - content: "\f0a0"; -} -.icon-bullhorn:before { - content: "\f0a1"; -} -.icon-bell:before { - content: "\f0a2"; -} -.icon-certificate:before { - content: "\f0a3"; -} -.icon-hand-right:before { - content: "\f0a4"; -} -.icon-hand-left:before { - content: "\f0a5"; -} -.icon-hand-up:before { - content: "\f0a6"; -} -.icon-hand-down:before { - content: "\f0a7"; -} -.icon-circle-arrow-left:before { - content: "\f0a8"; -} -.icon-circle-arrow-right:before { - content: "\f0a9"; -} -.icon-circle-arrow-up:before { - content: "\f0aa"; -} -.icon-circle-arrow-down:before { - content: "\f0ab"; -} -.icon-globe:before { - content: "\f0ac"; -} -.icon-wrench:before { - content: "\f0ad"; -} -.icon-tasks:before { - content: "\f0ae"; -} -.icon-filter:before { - content: "\f0b0"; -} -.icon-briefcase:before { - content: "\f0b1"; -} -.icon-fullscreen:before { - content: "\f0b2"; -} -.icon-group:before { - content: "\f0c0"; -} -.icon-link:before { - content: "\f0c1"; -} -.icon-cloud:before { - content: "\f0c2"; -} -.icon-beaker:before { - content: "\f0c3"; -} -.icon-cut:before { - content: "\f0c4"; -} -.icon-copy:before { - content: "\f0c5"; -} -.icon-paper-clip:before { - content: "\f0c6"; -} -.icon-save:before { - content: "\f0c7"; -} -.icon-sign-blank:before { - content: "\f0c8"; -} -.icon-reorder:before { - content: "\f0c9"; -} -.icon-list-ul:before { - content: "\f0ca"; -} -.icon-list-ol:before { - content: "\f0cb"; -} -.icon-strikethrough:before { - content: "\f0cc"; -} -.icon-underline:before { - content: "\f0cd"; -} -.icon-table:before { - content: "\f0ce"; -} -.icon-magic:before { - content: "\f0d0"; -} -.icon-truck:before { - content: "\f0d1"; -} -.icon-pinterest:before { - content: "\f0d2"; -} -.icon-pinterest-sign:before { - content: "\f0d3"; -} -.icon-google-plus-sign:before { - content: "\f0d4"; -} -.icon-google-plus:before { - content: "\f0d5"; -} -.icon-money:before { - content: "\f0d6"; -} -.icon-caret-down:before { - content: "\f0d7"; -} -.icon-caret-up:before { - content: "\f0d8"; -} -.icon-caret-left:before { - content: "\f0d9"; -} -.icon-caret-right:before { - content: "\f0da"; -} -.icon-columns:before { - content: "\f0db"; -} -.icon-sort:before { - content: "\f0dc"; -} -.icon-sort-down:before { - content: "\f0dd"; -} -.icon-sort-up:before { - content: "\f0de"; -} -.icon-envelope-alt:before { - content: "\f0e0"; -} -.icon-linkedin:before { - content: "\f0e1"; -} -.icon-undo:before { - content: "\f0e2"; -} -.icon-legal:before { - content: "\f0e3"; -} -.icon-dashboard:before { - content: "\f0e4"; -} -.icon-comment-alt:before { - content: "\f0e5"; -} -.icon-comments-alt:before { - content: "\f0e6"; -} -.icon-bolt:before { - content: "\f0e7"; -} -.icon-sitemap:before { - content: "\f0e8"; -} -.icon-umbrella:before { - content: "\f0e9"; -} -.icon-paste:before { - content: "\f0ea"; -} -.icon-lightbulb:before { - content: "\f0eb"; -} -.icon-exchange:before { - content: "\f0ec"; -} -.icon-cloud-download:before { - content: "\f0ed"; -} -.icon-cloud-upload:before { - content: "\f0ee"; -} -.icon-user-md:before { - content: "\f0f0"; -} -.icon-stethoscope:before { - content: "\f0f1"; -} -.icon-suitcase:before { - content: "\f0f2"; -} -.icon-bell-alt:before { - content: "\f0f3"; -} -.icon-coffee:before { - content: "\f0f4"; -} -.icon-food:before { - content: "\f0f5"; -} -.icon-file-alt:before { - content: "\f0f6"; -} -.icon-building:before { - content: "\f0f7"; -} -.icon-hospital:before { - content: "\f0f8"; -} -.icon-ambulance:before { - content: "\f0f9"; -} -.icon-medkit:before { - content: "\f0fa"; -} -.icon-fighter-jet:before { - content: "\f0fb"; -} -.icon-beer:before { - content: "\f0fc"; -} -.icon-h-sign:before { - content: "\f0fd"; -} -.icon-plus-sign-alt:before { - content: "\f0fe"; -} -.icon-double-angle-left:before { - content: "\f100"; -} -.icon-double-angle-right:before { - content: "\f101"; -} -.icon-double-angle-up:before { - content: "\f102"; -} -.icon-double-angle-down:before { - content: "\f103"; -} -.icon-angle-left:before { - content: "\f104"; -} -.icon-angle-right:before { - content: "\f105"; -} -.icon-angle-up:before { - content: "\f106"; -} -.icon-angle-down:before { - content: "\f107"; -} -.icon-desktop:before { - content: "\f108"; -} -.icon-laptop:before { - content: "\f109"; -} -.icon-tablet:before { - content: "\f10a"; -} -.icon-mobile-phone:before { - content: "\f10b"; -} -.icon-circle-blank:before { - content: "\f10c"; -} -.icon-quote-left:before { - content: "\f10d"; -} -.icon-quote-right:before { - content: "\f10e"; -} -.icon-spinner:before { - content: "\f110"; -} -.icon-circle:before { - content: "\f111"; -} -.icon-reply:before { - content: "\f112"; -} -.icon-github-alt:before { - content: "\f113"; -} -.icon-folder-close-alt:before { - content: "\f114"; -} -.icon-folder-open-alt:before { - content: "\f115"; -} [class^="icon-"], [class*=" icon-"] { display: inline-block; @@ -7963,45 +7141,39 @@ h4 small { .context-info.editing .module-content { margin-top: 0; } -.hero { - background: url("../../../base/images/background-tile.png"); +.homepage [role=main] { padding: 20px 0; min-height: 0; } -.hero > .container { +.homepage .row { position: relative; - padding-bottom: 0; } -.hero .search-giant { - margin-bottom: 10px; -} -.hero .search-giant input { - border-color: #003f52; -} -.hero .page-heading { - font-size: 18px; - margin-bottom: 0; -} -.hero .module-dark { +.homepage .module-search { padding: 5px; - margin-bottom: 0; + margin: 0; color: #ffffff; background: #ffffff; } -.hero .module-dark .module-content { +.homepage .module-search .search-giant { + margin-bottom: 10px; +} +.homepage .module-search .search-giant input { + border-color: #003f52; +} +.homepage .module-search .module-content { -webkit-border-radius: 3px 3px 0 0; -moz-border-radius: 3px 3px 0 0; border-radius: 3px 3px 0 0; background-color: #2f9b45; border-bottom: none; } -.hero .module-dark .module-content .heading { +.homepage .module-search .module-content .heading { margin-top: 0; margin-bottom: 7px; font-size: 24px; line-height: 40px; } -.hero .tags { +.homepage .module-search .tags { *zoom: 1; padding: 5px 10px 10px 10px; background-color: #237434; @@ -8009,69 +7181,77 @@ h4 small { -moz-border-radius: 0 0 3px 3px; border-radius: 0 0 3px 3px; } -.hero .tags:before, -.hero .tags:after { +.homepage .module-search .tags:before, +.homepage .module-search .tags:after { display: table; content: ""; line-height: 0; } -.hero .tags:after { +.homepage .module-search .tags:after { clear: both; } -.hero .tags h3, -.hero .tags .tag { +.homepage .module-search .tags h3, +.homepage .module-search .tags .tag { display: block; float: left; margin: 5px 10px 0 0; } -.hero .tags h3 { +.homepage .module-search .tags h3 { font-size: 14px; line-height: 20px; padding: 2px 8px; } -.hero-primary, -.hero-secondary { +.homepage .group-list { + margin: 0; +} +.homepage .box .inner { + padding: 20px 25px; +} +.homepage .stats h3 { + margin: 0 0 10px 0; +} +.homepage .stats ul { + margin: 0; + list-style: none; + *zoom: 1; +} +.homepage .stats ul:before, +.homepage .stats ul:after { + display: table; + content: ""; + line-height: 0; +} +.homepage .stats ul:after { + clear: both; +} +.homepage .stats ul li { float: left; - margin-left: 20px; - width: 460px; + width: 25%; + font-weight: 300; } -.hero-primary { - margin-left: 0; - margin-bottom: 0; +.homepage .stats ul li a { + display: block; } -.hero-secondary { +.homepage .stats ul li a b { + display: block; + font-size: 35px; + line-height: 1.5; +} +.homepage .stats ul li a:hover { + text-decoration: none; +} +.homepage.layout-1 .row1 .col2 { position: absolute; bottom: 0; right: 0; } -.hero-secondary .hero-secondary-inner { +.homepage.layout-1 .row1 .col2 .module-search { bottom: 0; left: 0; right: 0; } -.main.homepage { - padding-top: 20px; - padding-bottom: 20px; - border-top: 1px solid #cccccc; -} -.main.homepage .module-heading .media-image { - margin-right: 15px; - max-height: 53px; - -webkit-box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5); - -moz-box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5); - box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5); -} -.main.homepage .group-listing .box { - min-height: 275px; -} -.main.homepage .group-list { - margin-bottom: 0; -} -.main.homepage .group-list .dataset-content { - min-height: 70px; -} -.main.homepage .box .module { - margin-top: 0; +.homepage.layout-2 .stats { + margin-top: 20px; } .account-masthead { *zoom: 1; @@ -8920,6 +8100,11 @@ iframe { padding-bottom: 20px !important; margin-bottom: 0 !important; } +.ie8 .lang-dropdown, +.ie7 .lang-dropdown { + position: relative !important; + top: -90px !important; +} .ie7 .alert { position: relative; } diff --git a/ckan/public/base/css/main.css b/ckan/public/base/css/main.css index 2ed06e1eb12..77b93ef3cfc 100644 --- a/ckan/public/base/css/main.css +++ b/ckan/public/base/css/main.css @@ -7141,45 +7141,39 @@ h4 small { .context-info.editing .module-content { margin-top: 0; } -.hero { - background: url("../../../base/images/background-tile.png"); +.homepage [role=main] { padding: 20px 0; min-height: 0; } -.hero > .container { +.homepage .row { position: relative; - padding-bottom: 0; -} -.hero .search-giant { - margin-bottom: 10px; -} -.hero .search-giant input { - border-color: #003f52; } -.hero .page-heading { - font-size: 18px; - margin-bottom: 0; -} -.hero .module-dark { +.homepage .module-search { padding: 5px; - margin-bottom: 0; + margin: 0; color: #ffffff; background: #ffffff; } -.hero .module-dark .module-content { +.homepage .module-search .search-giant { + margin-bottom: 10px; +} +.homepage .module-search .search-giant input { + border-color: #003f52; +} +.homepage .module-search .module-content { -webkit-border-radius: 3px 3px 0 0; -moz-border-radius: 3px 3px 0 0; border-radius: 3px 3px 0 0; background-color: #005d7a; border-bottom: none; } -.hero .module-dark .module-content .heading { +.homepage .module-search .module-content .heading { margin-top: 0; margin-bottom: 7px; font-size: 24px; line-height: 40px; } -.hero .tags { +.homepage .module-search .tags { *zoom: 1; padding: 5px 10px 10px 10px; background-color: #003647; @@ -7187,69 +7181,77 @@ h4 small { -moz-border-radius: 0 0 3px 3px; border-radius: 0 0 3px 3px; } -.hero .tags:before, -.hero .tags:after { +.homepage .module-search .tags:before, +.homepage .module-search .tags:after { display: table; content: ""; line-height: 0; } -.hero .tags:after { +.homepage .module-search .tags:after { clear: both; } -.hero .tags h3, -.hero .tags .tag { +.homepage .module-search .tags h3, +.homepage .module-search .tags .tag { display: block; float: left; margin: 5px 10px 0 0; } -.hero .tags h3 { +.homepage .module-search .tags h3 { font-size: 14px; line-height: 20px; padding: 2px 8px; } -.hero-primary, -.hero-secondary { +.homepage .group-list { + margin: 0; +} +.homepage .box .inner { + padding: 20px 25px; +} +.homepage .stats h3 { + margin: 0 0 10px 0; +} +.homepage .stats ul { + margin: 0; + list-style: none; + *zoom: 1; +} +.homepage .stats ul:before, +.homepage .stats ul:after { + display: table; + content: ""; + line-height: 0; +} +.homepage .stats ul:after { + clear: both; +} +.homepage .stats ul li { float: left; - margin-left: 20px; - width: 460px; + width: 25%; + font-weight: 300; } -.hero-primary { - margin-left: 0; - margin-bottom: 0; +.homepage .stats ul li a { + display: block; +} +.homepage .stats ul li a b { + display: block; + font-size: 35px; + line-height: 1.5; } -.hero-secondary { +.homepage .stats ul li a:hover { + text-decoration: none; +} +.homepage.layout-1 .row1 .col2 { position: absolute; bottom: 0; right: 0; } -.hero-secondary .hero-secondary-inner { +.homepage.layout-1 .row1 .col2 .module-search { bottom: 0; left: 0; right: 0; } -.main.homepage { - padding-top: 20px; - padding-bottom: 20px; - border-top: 1px solid #cccccc; -} -.main.homepage .module-heading .media-image { - margin-right: 15px; - max-height: 53px; - -webkit-box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5); - -moz-box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5); - box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5); -} -.main.homepage .group-listing .box { - min-height: 275px; -} -.main.homepage .group-list { - margin-bottom: 0; -} -.main.homepage .group-list .dataset-content { - min-height: 70px; -} -.main.homepage .box .module { - margin-top: 0; +.homepage.layout-2 .stats { + margin-top: 20px; } .account-masthead { *zoom: 1; @@ -7925,9 +7927,9 @@ h4 small { -webkit-border-radius: 100px; -moz-border-radius: 100px; border-radius: 100px; - -webkit-box-shadow: inset 0 1px 2 x rgba(0, 0, 0, 0.2); - -moz-box-shadow: inset 0 1px 2 x rgba(0, 0, 0, 0.2); - box-shadow: inset 0 1px 2 x rgba(0, 0, 0, 0.2); + -webkit-box-shadow: inset 0 1px 2x rgba(0, 0, 0, 0.2); + -moz-box-shadow: inset 0 1px 2x rgba(0, 0, 0, 0.2); + box-shadow: inset 0 1px 2x rgba(0, 0, 0, 0.2); } .popover-followee .nav li a:hover i { background-color: #000; diff --git a/ckan/public/base/css/maroon.css b/ckan/public/base/css/maroon.css index c052d27ea74..585ed6129c2 100644 --- a/ckan/public/base/css/maroon.css +++ b/ckan/public/base/css/maroon.css @@ -2055,6 +2055,14 @@ table th[class*="span"], .open > .dropdown-menu { display: block; } +.dropdown-backdrop { + position: fixed; + left: 0; + right: 0; + bottom: 0; + top: 0; + z-index: 990; +} .pull-right > .dropdown-menu { right: 0; left: auto; @@ -5572,6 +5580,12 @@ textarea { .control-large input { height: 41px; } +.control-required { + color: #c6898b; +} +.form-actions .control-required-message { + float: left; +} .form-actions { background: none; margin-left: -25px; @@ -6904,842 +6918,6 @@ h4 small { height: 35px; background-position: -320px -62px; } -/* This is a modified version of the font-awesome core less document. */ -@font-face { - font-family: 'FontAwesome'; - src: url('../../../base/vendor/font-awesome/font/fontawesome-webfont.eot?v=3.0.1'); - src: url('../../../base/vendor/font-awesome/font/fontawesome-webfont.eot?#iefix&v=3.0.1') format('embedded-opentype'), url('../../../base/vendor/font-awesome/font/fontawesome-webfont.woff?v=3.0.1') format('woff'), url('../../../base/vendor/font-awesome/font/fontawesome-webfont.ttf?v=3.0.1') format('truetype'); - font-weight: normal; - font-style: normal; -} -[class^="icon-"], -[class*=" icon-"] { - font-family: FontAwesome; - font-weight: normal; - font-style: normal; - text-decoration: inherit; - -webkit-font-smoothing: antialiased; - display: inline; - width: auto; - height: auto; - line-height: normal; - vertical-align: baseline; - background-image: none; - background-position: 0% 0%; - background-repeat: repeat; - margin-top: 0; -} -.icon-spin { - display: inline-block; - -moz-animation: spin 2s infinite linear; - -o-animation: spin 2s infinite linear; - -webkit-animation: spin 2s infinite linear; - animation: spin 2s infinite linear; -} -@-moz-keyframes spin { - 0% { - -moz-transform: rotate(0deg); - } - 100% { - -moz-transform: rotate(359deg); - } -} -@-webkit-keyframes spin { - 0% { - -webkit-transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - } -} -@-o-keyframes spin { - 0% { - -o-transform: rotate(0deg); - } - 100% { - -o-transform: rotate(359deg); - } -} -@-ms-keyframes spin { - 0% { - -ms-transform: rotate(0deg); - } - 100% { - -ms-transform: rotate(359deg); - } -} -@keyframes spin { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(359deg); - } -} -@-moz-document url-prefix() { - .icon-spin { - height: .9em; - } - .btn .icon-spin { - height: auto; - } - .icon-spin.icon-large { - height: 1.25em; - } - .btn .icon-spin.icon-large { - height: .75em; - } -} -/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen - readers do not read off random characters that represent icons */ -.icon-glass:before { - content: "\f000"; -} -.icon-music:before { - content: "\f001"; -} -.icon-search:before { - content: "\f002"; -} -.icon-envelope:before { - content: "\f003"; -} -.icon-heart:before { - content: "\f004"; -} -.icon-star:before { - content: "\f005"; -} -.icon-star-empty:before { - content: "\f006"; -} -.icon-user:before { - content: "\f007"; -} -.icon-film:before { - content: "\f008"; -} -.icon-th-large:before { - content: "\f009"; -} -.icon-th:before { - content: "\f00a"; -} -.icon-th-list:before { - content: "\f00b"; -} -.icon-ok:before { - content: "\f00c"; -} -.icon-remove:before { - content: "\f00d"; -} -.icon-zoom-in:before { - content: "\f00e"; -} -.icon-zoom-out:before { - content: "\f010"; -} -.icon-off:before { - content: "\f011"; -} -.icon-signal:before { - content: "\f012"; -} -.icon-cog:before { - content: "\f013"; -} -.icon-trash:before { - content: "\f014"; -} -.icon-home:before { - content: "\f015"; -} -.icon-file:before { - content: "\f016"; -} -.icon-time:before { - content: "\f017"; -} -.icon-road:before { - content: "\f018"; -} -.icon-download-alt:before { - content: "\f019"; -} -.icon-download:before { - content: "\f01a"; -} -.icon-upload:before { - content: "\f01b"; -} -.icon-inbox:before { - content: "\f01c"; -} -.icon-play-circle:before { - content: "\f01d"; -} -.icon-repeat:before { - content: "\f01e"; -} -/* \f020 doesn't work in Safari. all shifted one down */ -.icon-refresh:before { - content: "\f021"; -} -.icon-list-alt:before { - content: "\f022"; -} -.icon-lock:before { - content: "\f023"; -} -.icon-flag:before { - content: "\f024"; -} -.icon-headphones:before { - content: "\f025"; -} -.icon-volume-off:before { - content: "\f026"; -} -.icon-volume-down:before { - content: "\f027"; -} -.icon-volume-up:before { - content: "\f028"; -} -.icon-qrcode:before { - content: "\f029"; -} -.icon-barcode:before { - content: "\f02a"; -} -.icon-tag:before { - content: "\f02b"; -} -.icon-tags:before { - content: "\f02c"; -} -.icon-book:before { - content: "\f02d"; -} -.icon-bookmark:before { - content: "\f02e"; -} -.icon-print:before { - content: "\f02f"; -} -.icon-camera:before { - content: "\f030"; -} -.icon-font:before { - content: "\f031"; -} -.icon-bold:before { - content: "\f032"; -} -.icon-italic:before { - content: "\f033"; -} -.icon-text-height:before { - content: "\f034"; -} -.icon-text-width:before { - content: "\f035"; -} -.icon-align-left:before { - content: "\f036"; -} -.icon-align-center:before { - content: "\f037"; -} -.icon-align-right:before { - content: "\f038"; -} -.icon-align-justify:before { - content: "\f039"; -} -.icon-list:before { - content: "\f03a"; -} -.icon-indent-left:before { - content: "\f03b"; -} -.icon-indent-right:before { - content: "\f03c"; -} -.icon-facetime-video:before { - content: "\f03d"; -} -.icon-picture:before { - content: "\f03e"; -} -.icon-pencil:before { - content: "\f040"; -} -.icon-map-marker:before { - content: "\f041"; -} -.icon-adjust:before { - content: "\f042"; -} -.icon-tint:before { - content: "\f043"; -} -.icon-edit:before { - content: "\f044"; -} -.icon-share:before { - content: "\f045"; -} -.icon-check:before { - content: "\f046"; -} -.icon-move:before { - content: "\f047"; -} -.icon-step-backward:before { - content: "\f048"; -} -.icon-fast-backward:before { - content: "\f049"; -} -.icon-backward:before { - content: "\f04a"; -} -.icon-play:before { - content: "\f04b"; -} -.icon-pause:before { - content: "\f04c"; -} -.icon-stop:before { - content: "\f04d"; -} -.icon-forward:before { - content: "\f04e"; -} -.icon-fast-forward:before { - content: "\f050"; -} -.icon-step-forward:before { - content: "\f051"; -} -.icon-eject:before { - content: "\f052"; -} -.icon-chevron-left:before { - content: "\f053"; -} -.icon-chevron-right:before { - content: "\f054"; -} -.icon-plus-sign:before { - content: "\f055"; -} -.icon-minus-sign:before { - content: "\f056"; -} -.icon-remove-sign:before { - content: "\f057"; -} -.icon-ok-sign:before { - content: "\f058"; -} -.icon-question-sign:before { - content: "\f059"; -} -.icon-info-sign:before { - content: "\f05a"; -} -.icon-screenshot:before { - content: "\f05b"; -} -.icon-remove-circle:before { - content: "\f05c"; -} -.icon-ok-circle:before { - content: "\f05d"; -} -.icon-ban-circle:before { - content: "\f05e"; -} -.icon-arrow-left:before { - content: "\f060"; -} -.icon-arrow-right:before { - content: "\f061"; -} -.icon-arrow-up:before { - content: "\f062"; -} -.icon-arrow-down:before { - content: "\f063"; -} -.icon-share-alt:before { - content: "\f064"; -} -.icon-resize-full:before { - content: "\f065"; -} -.icon-resize-small:before { - content: "\f066"; -} -.icon-plus:before { - content: "\f067"; -} -.icon-minus:before { - content: "\f068"; -} -.icon-asterisk:before { - content: "\f069"; -} -.icon-exclamation-sign:before { - content: "\f06a"; -} -.icon-gift:before { - content: "\f06b"; -} -.icon-leaf:before { - content: "\f06c"; -} -.icon-fire:before { - content: "\f06d"; -} -.icon-eye-open:before { - content: "\f06e"; -} -.icon-eye-close:before { - content: "\f070"; -} -.icon-warning-sign:before { - content: "\f071"; -} -.icon-plane:before { - content: "\f072"; -} -.icon-calendar:before { - content: "\f073"; -} -.icon-random:before { - content: "\f074"; -} -.icon-comment:before { - content: "\f075"; -} -.icon-magnet:before { - content: "\f076"; -} -.icon-chevron-up:before { - content: "\f077"; -} -.icon-chevron-down:before { - content: "\f078"; -} -.icon-retweet:before { - content: "\f079"; -} -.icon-shopping-cart:before { - content: "\f07a"; -} -.icon-folder-close:before { - content: "\f07b"; -} -.icon-folder-open:before { - content: "\f07c"; -} -.icon-resize-vertical:before { - content: "\f07d"; -} -.icon-resize-horizontal:before { - content: "\f07e"; -} -.icon-bar-chart:before { - content: "\f080"; -} -.icon-twitter-sign:before { - content: "\f081"; -} -.icon-facebook-sign:before { - content: "\f082"; -} -.icon-camera-retro:before { - content: "\f083"; -} -.icon-key:before { - content: "\f084"; -} -.icon-cogs:before { - content: "\f085"; -} -.icon-comments:before { - content: "\f086"; -} -.icon-thumbs-up:before { - content: "\f087"; -} -.icon-thumbs-down:before { - content: "\f088"; -} -.icon-star-half:before { - content: "\f089"; -} -.icon-heart-empty:before { - content: "\f08a"; -} -.icon-signout:before { - content: "\f08b"; -} -.icon-linkedin-sign:before { - content: "\f08c"; -} -.icon-pushpin:before { - content: "\f08d"; -} -.icon-external-link:before { - content: "\f08e"; -} -.icon-signin:before { - content: "\f090"; -} -.icon-trophy:before { - content: "\f091"; -} -.icon-github-sign:before { - content: "\f092"; -} -.icon-upload-alt:before { - content: "\f093"; -} -.icon-lemon:before { - content: "\f094"; -} -.icon-phone:before { - content: "\f095"; -} -.icon-check-empty:before { - content: "\f096"; -} -.icon-bookmark-empty:before { - content: "\f097"; -} -.icon-phone-sign:before { - content: "\f098"; -} -.icon-twitter:before { - content: "\f099"; -} -.icon-facebook:before { - content: "\f09a"; -} -.icon-github:before { - content: "\f09b"; -} -.icon-unlock:before { - content: "\f09c"; -} -.icon-credit-card:before { - content: "\f09d"; -} -.icon-rss:before { - content: "\f09e"; -} -.icon-hdd:before { - content: "\f0a0"; -} -.icon-bullhorn:before { - content: "\f0a1"; -} -.icon-bell:before { - content: "\f0a2"; -} -.icon-certificate:before { - content: "\f0a3"; -} -.icon-hand-right:before { - content: "\f0a4"; -} -.icon-hand-left:before { - content: "\f0a5"; -} -.icon-hand-up:before { - content: "\f0a6"; -} -.icon-hand-down:before { - content: "\f0a7"; -} -.icon-circle-arrow-left:before { - content: "\f0a8"; -} -.icon-circle-arrow-right:before { - content: "\f0a9"; -} -.icon-circle-arrow-up:before { - content: "\f0aa"; -} -.icon-circle-arrow-down:before { - content: "\f0ab"; -} -.icon-globe:before { - content: "\f0ac"; -} -.icon-wrench:before { - content: "\f0ad"; -} -.icon-tasks:before { - content: "\f0ae"; -} -.icon-filter:before { - content: "\f0b0"; -} -.icon-briefcase:before { - content: "\f0b1"; -} -.icon-fullscreen:before { - content: "\f0b2"; -} -.icon-group:before { - content: "\f0c0"; -} -.icon-link:before { - content: "\f0c1"; -} -.icon-cloud:before { - content: "\f0c2"; -} -.icon-beaker:before { - content: "\f0c3"; -} -.icon-cut:before { - content: "\f0c4"; -} -.icon-copy:before { - content: "\f0c5"; -} -.icon-paper-clip:before { - content: "\f0c6"; -} -.icon-save:before { - content: "\f0c7"; -} -.icon-sign-blank:before { - content: "\f0c8"; -} -.icon-reorder:before { - content: "\f0c9"; -} -.icon-list-ul:before { - content: "\f0ca"; -} -.icon-list-ol:before { - content: "\f0cb"; -} -.icon-strikethrough:before { - content: "\f0cc"; -} -.icon-underline:before { - content: "\f0cd"; -} -.icon-table:before { - content: "\f0ce"; -} -.icon-magic:before { - content: "\f0d0"; -} -.icon-truck:before { - content: "\f0d1"; -} -.icon-pinterest:before { - content: "\f0d2"; -} -.icon-pinterest-sign:before { - content: "\f0d3"; -} -.icon-google-plus-sign:before { - content: "\f0d4"; -} -.icon-google-plus:before { - content: "\f0d5"; -} -.icon-money:before { - content: "\f0d6"; -} -.icon-caret-down:before { - content: "\f0d7"; -} -.icon-caret-up:before { - content: "\f0d8"; -} -.icon-caret-left:before { - content: "\f0d9"; -} -.icon-caret-right:before { - content: "\f0da"; -} -.icon-columns:before { - content: "\f0db"; -} -.icon-sort:before { - content: "\f0dc"; -} -.icon-sort-down:before { - content: "\f0dd"; -} -.icon-sort-up:before { - content: "\f0de"; -} -.icon-envelope-alt:before { - content: "\f0e0"; -} -.icon-linkedin:before { - content: "\f0e1"; -} -.icon-undo:before { - content: "\f0e2"; -} -.icon-legal:before { - content: "\f0e3"; -} -.icon-dashboard:before { - content: "\f0e4"; -} -.icon-comment-alt:before { - content: "\f0e5"; -} -.icon-comments-alt:before { - content: "\f0e6"; -} -.icon-bolt:before { - content: "\f0e7"; -} -.icon-sitemap:before { - content: "\f0e8"; -} -.icon-umbrella:before { - content: "\f0e9"; -} -.icon-paste:before { - content: "\f0ea"; -} -.icon-lightbulb:before { - content: "\f0eb"; -} -.icon-exchange:before { - content: "\f0ec"; -} -.icon-cloud-download:before { - content: "\f0ed"; -} -.icon-cloud-upload:before { - content: "\f0ee"; -} -.icon-user-md:before { - content: "\f0f0"; -} -.icon-stethoscope:before { - content: "\f0f1"; -} -.icon-suitcase:before { - content: "\f0f2"; -} -.icon-bell-alt:before { - content: "\f0f3"; -} -.icon-coffee:before { - content: "\f0f4"; -} -.icon-food:before { - content: "\f0f5"; -} -.icon-file-alt:before { - content: "\f0f6"; -} -.icon-building:before { - content: "\f0f7"; -} -.icon-hospital:before { - content: "\f0f8"; -} -.icon-ambulance:before { - content: "\f0f9"; -} -.icon-medkit:before { - content: "\f0fa"; -} -.icon-fighter-jet:before { - content: "\f0fb"; -} -.icon-beer:before { - content: "\f0fc"; -} -.icon-h-sign:before { - content: "\f0fd"; -} -.icon-plus-sign-alt:before { - content: "\f0fe"; -} -.icon-double-angle-left:before { - content: "\f100"; -} -.icon-double-angle-right:before { - content: "\f101"; -} -.icon-double-angle-up:before { - content: "\f102"; -} -.icon-double-angle-down:before { - content: "\f103"; -} -.icon-angle-left:before { - content: "\f104"; -} -.icon-angle-right:before { - content: "\f105"; -} -.icon-angle-up:before { - content: "\f106"; -} -.icon-angle-down:before { - content: "\f107"; -} -.icon-desktop:before { - content: "\f108"; -} -.icon-laptop:before { - content: "\f109"; -} -.icon-tablet:before { - content: "\f10a"; -} -.icon-mobile-phone:before { - content: "\f10b"; -} -.icon-circle-blank:before { - content: "\f10c"; -} -.icon-quote-left:before { - content: "\f10d"; -} -.icon-quote-right:before { - content: "\f10e"; -} -.icon-spinner:before { - content: "\f110"; -} -.icon-circle:before { - content: "\f111"; -} -.icon-reply:before { - content: "\f112"; -} -.icon-github-alt:before { - content: "\f113"; -} -.icon-folder-close-alt:before { - content: "\f114"; -} -.icon-folder-open-alt:before { - content: "\f115"; -} [class^="icon-"], [class*=" icon-"] { display: inline-block; @@ -7963,45 +7141,39 @@ h4 small { .context-info.editing .module-content { margin-top: 0; } -.hero { - background: url("../../../base/images/background-tile.png"); +.homepage [role=main] { padding: 20px 0; min-height: 0; } -.hero > .container { +.homepage .row { position: relative; - padding-bottom: 0; } -.hero .search-giant { - margin-bottom: 10px; -} -.hero .search-giant input { - border-color: #003f52; -} -.hero .page-heading { - font-size: 18px; - margin-bottom: 0; -} -.hero .module-dark { +.homepage .module-search { padding: 5px; - margin-bottom: 0; + margin: 0; color: #ffffff; background: #ffffff; } -.hero .module-dark .module-content { +.homepage .module-search .search-giant { + margin-bottom: 10px; +} +.homepage .module-search .search-giant input { + border-color: #003f52; +} +.homepage .module-search .module-content { -webkit-border-radius: 3px 3px 0 0; -moz-border-radius: 3px 3px 0 0; border-radius: 3px 3px 0 0; background-color: #810606; border-bottom: none; } -.hero .module-dark .module-content .heading { +.homepage .module-search .module-content .heading { margin-top: 0; margin-bottom: 7px; font-size: 24px; line-height: 40px; } -.hero .tags { +.homepage .module-search .tags { *zoom: 1; padding: 5px 10px 10px 10px; background-color: #500404; @@ -8009,69 +7181,77 @@ h4 small { -moz-border-radius: 0 0 3px 3px; border-radius: 0 0 3px 3px; } -.hero .tags:before, -.hero .tags:after { +.homepage .module-search .tags:before, +.homepage .module-search .tags:after { display: table; content: ""; line-height: 0; } -.hero .tags:after { +.homepage .module-search .tags:after { clear: both; } -.hero .tags h3, -.hero .tags .tag { +.homepage .module-search .tags h3, +.homepage .module-search .tags .tag { display: block; float: left; margin: 5px 10px 0 0; } -.hero .tags h3 { +.homepage .module-search .tags h3 { font-size: 14px; line-height: 20px; padding: 2px 8px; } -.hero-primary, -.hero-secondary { +.homepage .group-list { + margin: 0; +} +.homepage .box .inner { + padding: 20px 25px; +} +.homepage .stats h3 { + margin: 0 0 10px 0; +} +.homepage .stats ul { + margin: 0; + list-style: none; + *zoom: 1; +} +.homepage .stats ul:before, +.homepage .stats ul:after { + display: table; + content: ""; + line-height: 0; +} +.homepage .stats ul:after { + clear: both; +} +.homepage .stats ul li { float: left; - margin-left: 20px; - width: 460px; + width: 25%; + font-weight: 300; } -.hero-primary { - margin-left: 0; - margin-bottom: 0; +.homepage .stats ul li a { + display: block; } -.hero-secondary { +.homepage .stats ul li a b { + display: block; + font-size: 35px; + line-height: 1.5; +} +.homepage .stats ul li a:hover { + text-decoration: none; +} +.homepage.layout-1 .row1 .col2 { position: absolute; bottom: 0; right: 0; } -.hero-secondary .hero-secondary-inner { +.homepage.layout-1 .row1 .col2 .module-search { bottom: 0; left: 0; right: 0; } -.main.homepage { - padding-top: 20px; - padding-bottom: 20px; - border-top: 1px solid #cccccc; -} -.main.homepage .module-heading .media-image { - margin-right: 15px; - max-height: 53px; - -webkit-box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5); - -moz-box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5); - box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5); -} -.main.homepage .group-listing .box { - min-height: 275px; -} -.main.homepage .group-list { - margin-bottom: 0; -} -.main.homepage .group-list .dataset-content { - min-height: 70px; -} -.main.homepage .box .module { - margin-top: 0; +.homepage.layout-2 .stats { + margin-top: 20px; } .account-masthead { *zoom: 1; @@ -8920,6 +8100,11 @@ iframe { padding-bottom: 20px !important; margin-bottom: 0 !important; } +.ie8 .lang-dropdown, +.ie7 .lang-dropdown { + position: relative !important; + top: -90px !important; +} .ie7 .alert { position: relative; } diff --git a/ckan/public/base/css/red.css b/ckan/public/base/css/red.css index 26f22422970..d24e241544a 100644 --- a/ckan/public/base/css/red.css +++ b/ckan/public/base/css/red.css @@ -2055,6 +2055,14 @@ table th[class*="span"], .open > .dropdown-menu { display: block; } +.dropdown-backdrop { + position: fixed; + left: 0; + right: 0; + bottom: 0; + top: 0; + z-index: 990; +} .pull-right > .dropdown-menu { right: 0; left: auto; @@ -5572,6 +5580,12 @@ textarea { .control-large input { height: 41px; } +.control-required { + color: #c6898b; +} +.form-actions .control-required-message { + float: left; +} .form-actions { background: none; margin-left: -25px; @@ -6904,842 +6918,6 @@ h4 small { height: 35px; background-position: -320px -62px; } -/* This is a modified version of the font-awesome core less document. */ -@font-face { - font-family: 'FontAwesome'; - src: url('../../../base/vendor/font-awesome/font/fontawesome-webfont.eot?v=3.0.1'); - src: url('../../../base/vendor/font-awesome/font/fontawesome-webfont.eot?#iefix&v=3.0.1') format('embedded-opentype'), url('../../../base/vendor/font-awesome/font/fontawesome-webfont.woff?v=3.0.1') format('woff'), url('../../../base/vendor/font-awesome/font/fontawesome-webfont.ttf?v=3.0.1') format('truetype'); - font-weight: normal; - font-style: normal; -} -[class^="icon-"], -[class*=" icon-"] { - font-family: FontAwesome; - font-weight: normal; - font-style: normal; - text-decoration: inherit; - -webkit-font-smoothing: antialiased; - display: inline; - width: auto; - height: auto; - line-height: normal; - vertical-align: baseline; - background-image: none; - background-position: 0% 0%; - background-repeat: repeat; - margin-top: 0; -} -.icon-spin { - display: inline-block; - -moz-animation: spin 2s infinite linear; - -o-animation: spin 2s infinite linear; - -webkit-animation: spin 2s infinite linear; - animation: spin 2s infinite linear; -} -@-moz-keyframes spin { - 0% { - -moz-transform: rotate(0deg); - } - 100% { - -moz-transform: rotate(359deg); - } -} -@-webkit-keyframes spin { - 0% { - -webkit-transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - } -} -@-o-keyframes spin { - 0% { - -o-transform: rotate(0deg); - } - 100% { - -o-transform: rotate(359deg); - } -} -@-ms-keyframes spin { - 0% { - -ms-transform: rotate(0deg); - } - 100% { - -ms-transform: rotate(359deg); - } -} -@keyframes spin { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(359deg); - } -} -@-moz-document url-prefix() { - .icon-spin { - height: .9em; - } - .btn .icon-spin { - height: auto; - } - .icon-spin.icon-large { - height: 1.25em; - } - .btn .icon-spin.icon-large { - height: .75em; - } -} -/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen - readers do not read off random characters that represent icons */ -.icon-glass:before { - content: "\f000"; -} -.icon-music:before { - content: "\f001"; -} -.icon-search:before { - content: "\f002"; -} -.icon-envelope:before { - content: "\f003"; -} -.icon-heart:before { - content: "\f004"; -} -.icon-star:before { - content: "\f005"; -} -.icon-star-empty:before { - content: "\f006"; -} -.icon-user:before { - content: "\f007"; -} -.icon-film:before { - content: "\f008"; -} -.icon-th-large:before { - content: "\f009"; -} -.icon-th:before { - content: "\f00a"; -} -.icon-th-list:before { - content: "\f00b"; -} -.icon-ok:before { - content: "\f00c"; -} -.icon-remove:before { - content: "\f00d"; -} -.icon-zoom-in:before { - content: "\f00e"; -} -.icon-zoom-out:before { - content: "\f010"; -} -.icon-off:before { - content: "\f011"; -} -.icon-signal:before { - content: "\f012"; -} -.icon-cog:before { - content: "\f013"; -} -.icon-trash:before { - content: "\f014"; -} -.icon-home:before { - content: "\f015"; -} -.icon-file:before { - content: "\f016"; -} -.icon-time:before { - content: "\f017"; -} -.icon-road:before { - content: "\f018"; -} -.icon-download-alt:before { - content: "\f019"; -} -.icon-download:before { - content: "\f01a"; -} -.icon-upload:before { - content: "\f01b"; -} -.icon-inbox:before { - content: "\f01c"; -} -.icon-play-circle:before { - content: "\f01d"; -} -.icon-repeat:before { - content: "\f01e"; -} -/* \f020 doesn't work in Safari. all shifted one down */ -.icon-refresh:before { - content: "\f021"; -} -.icon-list-alt:before { - content: "\f022"; -} -.icon-lock:before { - content: "\f023"; -} -.icon-flag:before { - content: "\f024"; -} -.icon-headphones:before { - content: "\f025"; -} -.icon-volume-off:before { - content: "\f026"; -} -.icon-volume-down:before { - content: "\f027"; -} -.icon-volume-up:before { - content: "\f028"; -} -.icon-qrcode:before { - content: "\f029"; -} -.icon-barcode:before { - content: "\f02a"; -} -.icon-tag:before { - content: "\f02b"; -} -.icon-tags:before { - content: "\f02c"; -} -.icon-book:before { - content: "\f02d"; -} -.icon-bookmark:before { - content: "\f02e"; -} -.icon-print:before { - content: "\f02f"; -} -.icon-camera:before { - content: "\f030"; -} -.icon-font:before { - content: "\f031"; -} -.icon-bold:before { - content: "\f032"; -} -.icon-italic:before { - content: "\f033"; -} -.icon-text-height:before { - content: "\f034"; -} -.icon-text-width:before { - content: "\f035"; -} -.icon-align-left:before { - content: "\f036"; -} -.icon-align-center:before { - content: "\f037"; -} -.icon-align-right:before { - content: "\f038"; -} -.icon-align-justify:before { - content: "\f039"; -} -.icon-list:before { - content: "\f03a"; -} -.icon-indent-left:before { - content: "\f03b"; -} -.icon-indent-right:before { - content: "\f03c"; -} -.icon-facetime-video:before { - content: "\f03d"; -} -.icon-picture:before { - content: "\f03e"; -} -.icon-pencil:before { - content: "\f040"; -} -.icon-map-marker:before { - content: "\f041"; -} -.icon-adjust:before { - content: "\f042"; -} -.icon-tint:before { - content: "\f043"; -} -.icon-edit:before { - content: "\f044"; -} -.icon-share:before { - content: "\f045"; -} -.icon-check:before { - content: "\f046"; -} -.icon-move:before { - content: "\f047"; -} -.icon-step-backward:before { - content: "\f048"; -} -.icon-fast-backward:before { - content: "\f049"; -} -.icon-backward:before { - content: "\f04a"; -} -.icon-play:before { - content: "\f04b"; -} -.icon-pause:before { - content: "\f04c"; -} -.icon-stop:before { - content: "\f04d"; -} -.icon-forward:before { - content: "\f04e"; -} -.icon-fast-forward:before { - content: "\f050"; -} -.icon-step-forward:before { - content: "\f051"; -} -.icon-eject:before { - content: "\f052"; -} -.icon-chevron-left:before { - content: "\f053"; -} -.icon-chevron-right:before { - content: "\f054"; -} -.icon-plus-sign:before { - content: "\f055"; -} -.icon-minus-sign:before { - content: "\f056"; -} -.icon-remove-sign:before { - content: "\f057"; -} -.icon-ok-sign:before { - content: "\f058"; -} -.icon-question-sign:before { - content: "\f059"; -} -.icon-info-sign:before { - content: "\f05a"; -} -.icon-screenshot:before { - content: "\f05b"; -} -.icon-remove-circle:before { - content: "\f05c"; -} -.icon-ok-circle:before { - content: "\f05d"; -} -.icon-ban-circle:before { - content: "\f05e"; -} -.icon-arrow-left:before { - content: "\f060"; -} -.icon-arrow-right:before { - content: "\f061"; -} -.icon-arrow-up:before { - content: "\f062"; -} -.icon-arrow-down:before { - content: "\f063"; -} -.icon-share-alt:before { - content: "\f064"; -} -.icon-resize-full:before { - content: "\f065"; -} -.icon-resize-small:before { - content: "\f066"; -} -.icon-plus:before { - content: "\f067"; -} -.icon-minus:before { - content: "\f068"; -} -.icon-asterisk:before { - content: "\f069"; -} -.icon-exclamation-sign:before { - content: "\f06a"; -} -.icon-gift:before { - content: "\f06b"; -} -.icon-leaf:before { - content: "\f06c"; -} -.icon-fire:before { - content: "\f06d"; -} -.icon-eye-open:before { - content: "\f06e"; -} -.icon-eye-close:before { - content: "\f070"; -} -.icon-warning-sign:before { - content: "\f071"; -} -.icon-plane:before { - content: "\f072"; -} -.icon-calendar:before { - content: "\f073"; -} -.icon-random:before { - content: "\f074"; -} -.icon-comment:before { - content: "\f075"; -} -.icon-magnet:before { - content: "\f076"; -} -.icon-chevron-up:before { - content: "\f077"; -} -.icon-chevron-down:before { - content: "\f078"; -} -.icon-retweet:before { - content: "\f079"; -} -.icon-shopping-cart:before { - content: "\f07a"; -} -.icon-folder-close:before { - content: "\f07b"; -} -.icon-folder-open:before { - content: "\f07c"; -} -.icon-resize-vertical:before { - content: "\f07d"; -} -.icon-resize-horizontal:before { - content: "\f07e"; -} -.icon-bar-chart:before { - content: "\f080"; -} -.icon-twitter-sign:before { - content: "\f081"; -} -.icon-facebook-sign:before { - content: "\f082"; -} -.icon-camera-retro:before { - content: "\f083"; -} -.icon-key:before { - content: "\f084"; -} -.icon-cogs:before { - content: "\f085"; -} -.icon-comments:before { - content: "\f086"; -} -.icon-thumbs-up:before { - content: "\f087"; -} -.icon-thumbs-down:before { - content: "\f088"; -} -.icon-star-half:before { - content: "\f089"; -} -.icon-heart-empty:before { - content: "\f08a"; -} -.icon-signout:before { - content: "\f08b"; -} -.icon-linkedin-sign:before { - content: "\f08c"; -} -.icon-pushpin:before { - content: "\f08d"; -} -.icon-external-link:before { - content: "\f08e"; -} -.icon-signin:before { - content: "\f090"; -} -.icon-trophy:before { - content: "\f091"; -} -.icon-github-sign:before { - content: "\f092"; -} -.icon-upload-alt:before { - content: "\f093"; -} -.icon-lemon:before { - content: "\f094"; -} -.icon-phone:before { - content: "\f095"; -} -.icon-check-empty:before { - content: "\f096"; -} -.icon-bookmark-empty:before { - content: "\f097"; -} -.icon-phone-sign:before { - content: "\f098"; -} -.icon-twitter:before { - content: "\f099"; -} -.icon-facebook:before { - content: "\f09a"; -} -.icon-github:before { - content: "\f09b"; -} -.icon-unlock:before { - content: "\f09c"; -} -.icon-credit-card:before { - content: "\f09d"; -} -.icon-rss:before { - content: "\f09e"; -} -.icon-hdd:before { - content: "\f0a0"; -} -.icon-bullhorn:before { - content: "\f0a1"; -} -.icon-bell:before { - content: "\f0a2"; -} -.icon-certificate:before { - content: "\f0a3"; -} -.icon-hand-right:before { - content: "\f0a4"; -} -.icon-hand-left:before { - content: "\f0a5"; -} -.icon-hand-up:before { - content: "\f0a6"; -} -.icon-hand-down:before { - content: "\f0a7"; -} -.icon-circle-arrow-left:before { - content: "\f0a8"; -} -.icon-circle-arrow-right:before { - content: "\f0a9"; -} -.icon-circle-arrow-up:before { - content: "\f0aa"; -} -.icon-circle-arrow-down:before { - content: "\f0ab"; -} -.icon-globe:before { - content: "\f0ac"; -} -.icon-wrench:before { - content: "\f0ad"; -} -.icon-tasks:before { - content: "\f0ae"; -} -.icon-filter:before { - content: "\f0b0"; -} -.icon-briefcase:before { - content: "\f0b1"; -} -.icon-fullscreen:before { - content: "\f0b2"; -} -.icon-group:before { - content: "\f0c0"; -} -.icon-link:before { - content: "\f0c1"; -} -.icon-cloud:before { - content: "\f0c2"; -} -.icon-beaker:before { - content: "\f0c3"; -} -.icon-cut:before { - content: "\f0c4"; -} -.icon-copy:before { - content: "\f0c5"; -} -.icon-paper-clip:before { - content: "\f0c6"; -} -.icon-save:before { - content: "\f0c7"; -} -.icon-sign-blank:before { - content: "\f0c8"; -} -.icon-reorder:before { - content: "\f0c9"; -} -.icon-list-ul:before { - content: "\f0ca"; -} -.icon-list-ol:before { - content: "\f0cb"; -} -.icon-strikethrough:before { - content: "\f0cc"; -} -.icon-underline:before { - content: "\f0cd"; -} -.icon-table:before { - content: "\f0ce"; -} -.icon-magic:before { - content: "\f0d0"; -} -.icon-truck:before { - content: "\f0d1"; -} -.icon-pinterest:before { - content: "\f0d2"; -} -.icon-pinterest-sign:before { - content: "\f0d3"; -} -.icon-google-plus-sign:before { - content: "\f0d4"; -} -.icon-google-plus:before { - content: "\f0d5"; -} -.icon-money:before { - content: "\f0d6"; -} -.icon-caret-down:before { - content: "\f0d7"; -} -.icon-caret-up:before { - content: "\f0d8"; -} -.icon-caret-left:before { - content: "\f0d9"; -} -.icon-caret-right:before { - content: "\f0da"; -} -.icon-columns:before { - content: "\f0db"; -} -.icon-sort:before { - content: "\f0dc"; -} -.icon-sort-down:before { - content: "\f0dd"; -} -.icon-sort-up:before { - content: "\f0de"; -} -.icon-envelope-alt:before { - content: "\f0e0"; -} -.icon-linkedin:before { - content: "\f0e1"; -} -.icon-undo:before { - content: "\f0e2"; -} -.icon-legal:before { - content: "\f0e3"; -} -.icon-dashboard:before { - content: "\f0e4"; -} -.icon-comment-alt:before { - content: "\f0e5"; -} -.icon-comments-alt:before { - content: "\f0e6"; -} -.icon-bolt:before { - content: "\f0e7"; -} -.icon-sitemap:before { - content: "\f0e8"; -} -.icon-umbrella:before { - content: "\f0e9"; -} -.icon-paste:before { - content: "\f0ea"; -} -.icon-lightbulb:before { - content: "\f0eb"; -} -.icon-exchange:before { - content: "\f0ec"; -} -.icon-cloud-download:before { - content: "\f0ed"; -} -.icon-cloud-upload:before { - content: "\f0ee"; -} -.icon-user-md:before { - content: "\f0f0"; -} -.icon-stethoscope:before { - content: "\f0f1"; -} -.icon-suitcase:before { - content: "\f0f2"; -} -.icon-bell-alt:before { - content: "\f0f3"; -} -.icon-coffee:before { - content: "\f0f4"; -} -.icon-food:before { - content: "\f0f5"; -} -.icon-file-alt:before { - content: "\f0f6"; -} -.icon-building:before { - content: "\f0f7"; -} -.icon-hospital:before { - content: "\f0f8"; -} -.icon-ambulance:before { - content: "\f0f9"; -} -.icon-medkit:before { - content: "\f0fa"; -} -.icon-fighter-jet:before { - content: "\f0fb"; -} -.icon-beer:before { - content: "\f0fc"; -} -.icon-h-sign:before { - content: "\f0fd"; -} -.icon-plus-sign-alt:before { - content: "\f0fe"; -} -.icon-double-angle-left:before { - content: "\f100"; -} -.icon-double-angle-right:before { - content: "\f101"; -} -.icon-double-angle-up:before { - content: "\f102"; -} -.icon-double-angle-down:before { - content: "\f103"; -} -.icon-angle-left:before { - content: "\f104"; -} -.icon-angle-right:before { - content: "\f105"; -} -.icon-angle-up:before { - content: "\f106"; -} -.icon-angle-down:before { - content: "\f107"; -} -.icon-desktop:before { - content: "\f108"; -} -.icon-laptop:before { - content: "\f109"; -} -.icon-tablet:before { - content: "\f10a"; -} -.icon-mobile-phone:before { - content: "\f10b"; -} -.icon-circle-blank:before { - content: "\f10c"; -} -.icon-quote-left:before { - content: "\f10d"; -} -.icon-quote-right:before { - content: "\f10e"; -} -.icon-spinner:before { - content: "\f110"; -} -.icon-circle:before { - content: "\f111"; -} -.icon-reply:before { - content: "\f112"; -} -.icon-github-alt:before { - content: "\f113"; -} -.icon-folder-close-alt:before { - content: "\f114"; -} -.icon-folder-open-alt:before { - content: "\f115"; -} [class^="icon-"], [class*=" icon-"] { display: inline-block; @@ -7963,45 +7141,39 @@ h4 small { .context-info.editing .module-content { margin-top: 0; } -.hero { - background: url("../../../base/images/background-tile.png"); +.homepage [role=main] { padding: 20px 0; min-height: 0; } -.hero > .container { +.homepage .row { position: relative; - padding-bottom: 0; } -.hero .search-giant { - margin-bottom: 10px; -} -.hero .search-giant input { - border-color: #003f52; -} -.hero .page-heading { - font-size: 18px; - margin-bottom: 0; -} -.hero .module-dark { +.homepage .module-search { padding: 5px; - margin-bottom: 0; + margin: 0; color: #ffffff; background: #ffffff; } -.hero .module-dark .module-content { +.homepage .module-search .search-giant { + margin-bottom: 10px; +} +.homepage .module-search .search-giant input { + border-color: #003f52; +} +.homepage .module-search .module-content { -webkit-border-radius: 3px 3px 0 0; -moz-border-radius: 3px 3px 0 0; border-radius: 3px 3px 0 0; background-color: #c14531; border-bottom: none; } -.hero .module-dark .module-content .heading { +.homepage .module-search .module-content .heading { margin-top: 0; margin-bottom: 7px; font-size: 24px; line-height: 40px; } -.hero .tags { +.homepage .module-search .tags { *zoom: 1; padding: 5px 10px 10px 10px; background-color: #983627; @@ -8009,69 +7181,77 @@ h4 small { -moz-border-radius: 0 0 3px 3px; border-radius: 0 0 3px 3px; } -.hero .tags:before, -.hero .tags:after { +.homepage .module-search .tags:before, +.homepage .module-search .tags:after { display: table; content: ""; line-height: 0; } -.hero .tags:after { +.homepage .module-search .tags:after { clear: both; } -.hero .tags h3, -.hero .tags .tag { +.homepage .module-search .tags h3, +.homepage .module-search .tags .tag { display: block; float: left; margin: 5px 10px 0 0; } -.hero .tags h3 { +.homepage .module-search .tags h3 { font-size: 14px; line-height: 20px; padding: 2px 8px; } -.hero-primary, -.hero-secondary { +.homepage .group-list { + margin: 0; +} +.homepage .box .inner { + padding: 20px 25px; +} +.homepage .stats h3 { + margin: 0 0 10px 0; +} +.homepage .stats ul { + margin: 0; + list-style: none; + *zoom: 1; +} +.homepage .stats ul:before, +.homepage .stats ul:after { + display: table; + content: ""; + line-height: 0; +} +.homepage .stats ul:after { + clear: both; +} +.homepage .stats ul li { float: left; - margin-left: 20px; - width: 460px; + width: 25%; + font-weight: 300; } -.hero-primary { - margin-left: 0; - margin-bottom: 0; +.homepage .stats ul li a { + display: block; } -.hero-secondary { +.homepage .stats ul li a b { + display: block; + font-size: 35px; + line-height: 1.5; +} +.homepage .stats ul li a:hover { + text-decoration: none; +} +.homepage.layout-1 .row1 .col2 { position: absolute; bottom: 0; right: 0; } -.hero-secondary .hero-secondary-inner { +.homepage.layout-1 .row1 .col2 .module-search { bottom: 0; left: 0; right: 0; } -.main.homepage { - padding-top: 20px; - padding-bottom: 20px; - border-top: 1px solid #cccccc; -} -.main.homepage .module-heading .media-image { - margin-right: 15px; - max-height: 53px; - -webkit-box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5); - -moz-box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5); - box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5); -} -.main.homepage .group-listing .box { - min-height: 275px; -} -.main.homepage .group-list { - margin-bottom: 0; -} -.main.homepage .group-list .dataset-content { - min-height: 70px; -} -.main.homepage .box .module { - margin-top: 0; +.homepage.layout-2 .stats { + margin-top: 20px; } .account-masthead { *zoom: 1; @@ -8920,6 +8100,11 @@ iframe { padding-bottom: 20px !important; margin-bottom: 0 !important; } +.ie8 .lang-dropdown, +.ie7 .lang-dropdown { + position: relative !important; + top: -90px !important; +} .ie7 .alert { position: relative; }