diff --git a/ckan/controllers/group.py b/ckan/controllers/group.py index 57395125e2c..4e9e2ea5585 100644 --- a/ckan/controllers/group.py +++ b/ckan/controllers/group.py @@ -628,9 +628,8 @@ def activity(self, id, offset=0): # Add the group's activity stream (already rendered to HTML) to the # template context for the group/read.html template to retrieve later. - c.group_activity_stream = \ - get_action('group_activity_list_html')(context, - {'id': c.group_dict['id'], 'offset': offset}) + c.group_activity_stream = get_action('group_activity_list_html')( + context, {'id': c.group_dict['id'], 'offset': offset}) return render('group/activity_stream.html') diff --git a/ckan/controllers/user.py b/ckan/controllers/user.py index ca5f4899577..b0e13dccd2c 100644 --- a/ckan/controllers/user.py +++ b/ckan/controllers/user.py @@ -503,13 +503,74 @@ def activity(self, id, offset=0): return render('user/activity_stream.html') + def _get_dashboard_context(self, filter_type=None, filter_id=None, + q=None): + '''Return a dict needed by the dashboard view to determine context.''' + + def display_name(followee): + '''Return a display name for a user, group or dataset dict.''' + display_name = followee.get('display_name') + fullname = followee.get('fullname') + title = followee.get('title') + name = followee.get('name') + return display_name or fullname or title or name + + if (filter_type and filter_id): + context = {'model': model, 'session': model.Session, + 'user': c.user or c.author, 'for_view': True} + data_dict = {'id': filter_id} + followee = None + + action_functions = { + 'dataset': 'package_show', + 'user': 'user_show', + 'group': 'group_show' + } + action_function = logic.get_action(action_functions.get(filter_type)) + # Is this a valid type? + if action_function is None: + raise abort(404, _('Follow item not found')) + try: + followee = action_function(context, data_dict) + except NotFound: + abort(404, _('{0} not found').format(filter_type)) + except NotAuthorized: + abort(401, _('Unauthorized to read {0} {1}').format( + filter_type, id)) + + if followee is not None: + return { + 'filter_type': filter_type, + 'q': q, + 'context': display_name(followee), + 'selected_id': followee.get('id'), + 'dict': followee, + } + + return { + 'filter_type': filter_type, + 'q': q, + 'context': _('Everything'), + 'selected_id': False, + 'dict': None, + } + def dashboard(self, id=None, offset=0): context = {'model': model, 'session': model.Session, 'user': c.user or c.author, 'for_view': True} data_dict = {'id': id, 'user_obj': c.userobj, 'offset': offset} self._setup_template_variables(context, data_dict) - c.dashboard_activity_stream = h.dashboard_activity_stream(id, offset) + q = request.params.get('q', u'') + filter_type = request.params.get('type', u'') + filter_id = request.params.get('name', u'') + + c.followee_list = get_action('followee_list')( + context, {'id': c.userobj.id, 'q': q}) + c.dashboard_activity_stream_context = self._get_dashboard_context( + filter_type, filter_id, q) + c.dashboard_activity_stream = h.dashboard_activity_stream( + id, filter_type, filter_id, offset) # Mark the user's new activities as old whenever they view their # dashboard page. diff --git a/ckan/lib/app_globals.py b/ckan/lib/app_globals.py index f93a11e73f5..4778ad92b10 100644 --- a/ckan/lib/app_globals.py +++ b/ckan/lib/app_globals.py @@ -49,7 +49,7 @@ 'type': 'split', 'name': 'facets'}, 'package_hide_extras': {'type': 'split'}, - 'plugins': {'type': 'split'}, + 'ckan.plugins': {'type': 'split'}, # bool 'openid_enabled': {'default': 'true', 'type' : 'bool'}, diff --git a/ckan/lib/dictization/model_dictize.py b/ckan/lib/dictization/model_dictize.py index 490b5601fc5..a50edb569b6 100644 --- a/ckan/lib/dictization/model_dictize.py +++ b/ckan/lib/dictization/model_dictize.py @@ -220,6 +220,7 @@ def package_dictize(pkg, context): q = q.where(resource_group.c.package_id == pkg.id) result = _execute_with_revision(q, res_rev, context) result_dict["resources"] = resource_list_dictize(result, context) + result_dict['num_resources'] = len(result_dict.get('resources', [])) #tags tag_rev = model.package_tag_revision_table @@ -229,6 +230,7 @@ def package_dictize(pkg, context): ).where(tag_rev.c.package_id == pkg.id) result = _execute_with_revision(q, tag_rev, context) result_dict["tags"] = d.obj_list_dictize(result, context, lambda x: x["name"]) + result_dict['num_tags'] = len(result_dict.get('tags', [])) # Add display_names to tags. At first a tag's display_name is just the # same as its name, but the display_name might get changed later (e.g. diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py index c0f63d0c143..c4ac5a4ba8e 100644 --- a/ckan/lib/helpers.py +++ b/ckan/lib/helpers.py @@ -568,6 +568,8 @@ def check_access(action, data_dict=None): context = {'model': model, 'user': c.user or c.author} + if not data_dict: + data_dict = {} try: check_access_logic(action, context, data_dict) @@ -1255,21 +1257,37 @@ def user_in_org_or_group(group_id): return len(query.all()) != 0 -def dashboard_activity_stream(user_id, offset=0): +def dashboard_activity_stream(user_id, filter_type=None, filter_id=None, + offset=0): '''Return the dashboard activity stream of the given user. :param user_id: the id of the user :type user_id: string + :param filter_type: the type of thing to filter by + :type filter_type: string + + :param filter_id: the id of item to filter by + :type filter_id: string + :returns: an activity stream as an HTML snippet :rtype: string ''' import ckan.logic as logic context = {'model': model, 'session': model.Session, 'user': c.user} - return logic.get_action('dashboard_activity_list_html')(context, - {'id': user_id, - 'offset': offset}) + + if filter_type: + action_functions = { + 'dataset': 'package_activity_list_html', + 'user': 'user_activity_list_html', + 'group': 'group_activity_list_html' + } + action_function = logic.get_action(action_functions.get(filter_type)) + return action_function(context, {'id': filter_id, 'offset': offset}) + else: + return logic.get_action('dashboard_activity_list_html')( + context, {'id': user_id, 'offset': offset}) def recently_changed_packages_activity_stream(): @@ -1362,8 +1380,8 @@ def resource_preview(resource, pkg_id): Returns a rendered snippet for a embedded resource preview. Depending on the type, different previews are loaded. - This could be an img tag where the image is loaded directly or an iframe that - embeds a web page, recline or a pdf preview. + This could be an img tag where the image is loaded directly or an iframe + that embeds a web page, recline or a pdf preview. ''' format_lower = resource['format'].lower() @@ -1374,11 +1392,9 @@ def resource_preview(resource, pkg_id): if not resource['url']: log.info('No url for resource {0} defined.'.format(resource['id'])) - return snippet( - "dataviewer/snippets/no_preview.html", - resource_type=format_lower, - reason='No valid resource url has been defined.' - ) + return snippet("dataviewer/snippets/no_preview.html", + resource_type=format_lower, + reason='No valid resource url has been defined.') direct_embed = config.get('ckan.preview.direct', '').split() if not direct_embed: direct_embed = datapreview.DEFAULT_DIRECT_EMBED @@ -1388,24 +1404,23 @@ def resource_preview(resource, pkg_id): if datapreview.can_be_previewed(data_dict): url = url_for(controller='package', action='resource_datapreview', - resource_id=resource['id'], id=pkg_id, qualified=True) + resource_id=resource['id'], id=pkg_id, qualified=True) elif format_lower in direct_embed: directly = True url = resource['url'] elif format_lower in loadable_in_iframe: url = resource['url'] else: - log.info('No preview handler for resource type {0}'.format(format_lower)) - return snippet( - "dataviewer/snippets/no_preview.html", - resource_type=format_lower - ) - - return snippet( - "dataviewer/snippets/data_preview.html", - embed=directly, - resource_url=url + log.info( + 'No preview handler for resource type {0}'.format(format_lower) ) + return snippet("dataviewer/snippets/no_preview.html", + resource_type=format_lower) + + return snippet("dataviewer/snippets/data_preview.html", + embed=directly, + resource_url=url, + raw_resource_url=resource.get('url')) def SI_number_span(number): diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index 01e9095a2ba..e5bbb97d117 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -1861,12 +1861,17 @@ def user_activity_list(context, data_dict): _check_access('user_show', context, data_dict) model = context['model'] - user_id = _get_or_bust(data_dict, 'id') + + user_ref = _get_or_bust(data_dict, 'id') # May be user name or id. + user = model.User.get(user_ref) + if user is None: + raise logic.NotFound + offset = int(data_dict.get('offset', 0)) limit = int( data_dict.get('limit', config.get('ckan.activity_list_limit', 31))) - activity_objects = model.activity.user_activity_list(user_id, limit=limit, + activity_objects = model.activity.user_activity_list(user.id, limit=limit, offset=offset) return model_dictize.activity_list_dictize(activity_objects, context) @@ -1893,12 +1898,17 @@ def package_activity_list(context, data_dict): _check_access('package_show', context, data_dict) model = context['model'] - package_id = _get_or_bust(data_dict, 'id') + + package_ref = _get_or_bust(data_dict, 'id') # May be name or ID. + package = model.Package.get(package_ref) + if package is None: + raise logic.NotFound + offset = int(data_dict.get('offset', 0)) limit = int( data_dict.get('limit', config.get('ckan.activity_list_limit', 31))) - activity_objects = model.activity.package_activity_list(package_id, + activity_objects = model.activity.package_activity_list(package.id, limit=limit, offset=offset) return model_dictize.activity_list_dictize(activity_objects, context) @@ -1947,19 +1957,22 @@ def organization_activity_list(context, data_dict): :rtype: list of dictionaries ''' - # FIXME This is a duplicate of group_activity_list and - # package_activity_list but they claim to get the group/package by name - # or id but I think that they only get by id. Either they need fixing - # and remain seperate or they should share code - probably should share - # code anyway. + # FIXME: Filter out activities whose subject or object the user is not + # authorized to read. + _check_access('organization_show', context, data_dict) model = context['model'] - group_id = _get_or_bust(data_dict, 'id') - query = model.Session.query(model.Activity) - query = query.filter_by(object_id=group_id) - query = query.order_by(_desc(model.Activity.timestamp)) - query = query.limit(15) - activity_objects = query.all() + org_id = _get_or_bust(data_dict, 'id') + offset = int(data_dict.get('offset', 0)) + limit = int( + data_dict.get('limit', config.get('ckan.activity_list_limit', 31))) + + # Convert org_id (could be id or name) into id. + org_show = logic.get_action('organization_show') + org_id = org_show(context, {'id': org_id})['id'] + + activity_objects = model.activity.group_activity_list(org_id, + limit=limit, offset=offset) return model_dictize.activity_list_dictize(activity_objects, context) def recently_changed_packages_activity_list(context, data_dict): @@ -2107,7 +2120,6 @@ def organization_activity_list_html(context, data_dict): :rtype: string ''' - # FIXME does this work with a name? same issue with package/group activity_stream = organization_activity_list(context, data_dict) return activity_streams.activity_list_to_html(context, activity_stream) @@ -2319,14 +2331,43 @@ def am_following_group(context, data_dict): def _followee_count(context, data_dict, FollowerClass): - schema = context.get('schema', - ckan.logic.schema.default_follow_user_schema()) - data_dict, errors = _validate(data_dict, schema, context) - if errors: - raise ValidationError(errors) + if not context.get('skip_validation'): + schema = context.get('schema', + ckan.logic.schema.default_follow_user_schema()) + data_dict, errors = _validate(data_dict, schema, context) + if errors: + raise ValidationError(errors) return FollowerClass.followee_count(data_dict['id']) +def followee_count(context, data_dict): + '''Return the number of objects that are followed by the given user. + + Counts all objects, of any type, that the given user is following + (e.g. followed users, followed datasets, followed groups). + + :param id: the id of the user + :type id: string + + :rtype: int + + ''' + model = context['model'] + followee_users = _followee_count(context, data_dict, + model.UserFollowingUser) + + # followee_users has validated data_dict so the following functions don't + # need to validate it again. + context['skip_validation'] = True + + followee_datasets = _followee_count(context, data_dict, + model.UserFollowingDataset) + followee_groups = _followee_count(context, data_dict, + model.UserFollowingGroup) + + return sum((followee_users, followee_datasets, followee_groups)) + + def user_followee_count(context, data_dict): '''Return the number of users that are followed by the given user. @@ -2366,22 +2407,91 @@ def group_followee_count(context, data_dict): context['model'].UserFollowingGroup) -def user_followee_list(context, data_dict): - '''Return the list of users that are followed by the given user. +def followee_list(context, data_dict): + '''Return the list of objects that are followed by the given user. + + Returns all objects, of any type, that the given user is following + (e.g. followed users, followed datasets, followed groups.. ). :param id: the id of the user :type id: string - :rtype: list of dictionaries + :param q: a query string to limit results by, only objects whose display + name begins with the given string (case-insensitive) wil be returned + (optional) + :type q: string + + :rtype: list of dictionaries, each with keys 'type' (e.g. 'user', + 'dataset' or 'group'), 'display_name' (e.g. a user's display name, + or a package's title) and 'dict' (e.g. a dict representing the + followed user, package or group, the same as the dict that would be + returned by user_show, package_show or group_show) ''' - _check_access('user_followee_list', context, data_dict) + _check_access('followee_list', context, data_dict) schema = context.get('schema') or ( ckan.logic.schema.default_follow_user_schema()) data_dict, errors = _validate(data_dict, schema, context) if errors: raise ValidationError(errors) + def display_name(followee): + '''Return a display name for the given user, group or dataset dict.''' + display_name = followee.get('display_name') + fullname = followee.get('fullname') + title = followee.get('title') + name = followee.get('name') + return display_name or fullname or title or name + + # Get the followed objects. + # TODO: Catch exceptions raised by these *_followee_list() functions? + followee_dicts = [] + context['skip_validation'] = True + context['skip_authorization'] = True + for followee_list_function, followee_type in ( + (user_followee_list, 'user'), + (dataset_followee_list, 'dataset'), + (group_followee_list, 'group')): + dicts = followee_list_function(context, data_dict) + for d in dicts: + followee_dicts.append( + {'type': followee_type, + 'display_name': display_name(d), + 'dict': d}) + + followee_dicts.sort(key=lambda d: d['display_name']) + + q = data_dict.get('q') + if q: + q = q.strip().lower() + matching_followee_dicts = [] + for followee_dict in followee_dicts: + if followee_dict['display_name'].strip().lower().startswith(q): + matching_followee_dicts.append(followee_dict) + followee_dicts = matching_followee_dicts + + return followee_dicts + + +def user_followee_list(context, data_dict): + '''Return the list of users that are followed by the given user. + + :param id: the id of the user + :type id: string + + :rtype: list of dictionaries + + ''' + if not context.get('skip_authorization'): + _check_access('user_followee_list', context, data_dict) + + if not context.get('skip_validation'): + schema = context.get('schema') or ( + ckan.logic.schema.default_follow_user_schema()) + data_dict, errors = _validate(data_dict, schema, context) + if errors: + raise ValidationError(errors) + # Get the list of Follower objects. model = context['model'] user_id = data_dict.get('id') @@ -2403,12 +2513,15 @@ def dataset_followee_list(context, data_dict): :rtype: list of dictionaries ''' - _check_access('dataset_followee_list', context, data_dict) - schema = context.get('schema') or ( - ckan.logic.schema.default_follow_user_schema()) - data_dict, errors = _validate(data_dict, schema, context) - if errors: - raise ValidationError(errors) + if not context.get('skip_authorization'): + _check_access('dataset_followee_list', context, data_dict) + + if not context.get('skip_validation'): + schema = context.get('schema') or ( + ckan.logic.schema.default_follow_user_schema()) + data_dict, errors = _validate(data_dict, schema, context) + if errors: + raise ValidationError(errors) # Get the list of Follower objects. model = context['model'] @@ -2432,12 +2545,15 @@ def group_followee_list(context, data_dict): :rtype: list of dictionaries ''' - _check_access('group_followee_list', context, data_dict) - schema = context.get('schema', - ckan.logic.schema.default_follow_user_schema()) - data_dict, errors = _validate(data_dict, schema, context) - if errors: - raise ValidationError(errors) + if not context.get('skip_authorization'): + _check_access('group_followee_list', context, data_dict) + + if not context.get('skip_validation'): + schema = context.get('schema', + ckan.logic.schema.default_follow_user_schema()) + data_dict, errors = _validate(data_dict, schema, context) + if errors: + raise ValidationError(errors) # Get the list of UserFollowingGroup objects. model = context['model'] diff --git a/ckan/logic/auth/get.py b/ckan/logic/auth/get.py index c6412bc27c9..3df5689c2ea 100644 --- a/ckan/logic/auth/get.py +++ b/ckan/logic/auth/get.py @@ -248,6 +248,10 @@ def _followee_list(context, data_dict): return sysadmin(context, data_dict) +def followee_list(context, data_dict): + return _followee_list(context, data_dict) + + def user_followee_list(context, data_dict): return _followee_list(context, data_dict) diff --git a/ckan/logic/schema.py b/ckan/logic/schema.py index 53f084f6d4b..825a302496e 100644 --- a/ckan/logic/schema.py +++ b/ckan/logic/schema.py @@ -76,7 +76,7 @@ def default_resource_schema(): 'last_modified': [ignore_missing, isodate], 'cache_last_updated': [ignore_missing, isodate], 'webstore_last_updated': [ignore_missing, isodate], - 'tracking_summary': [ignore], + 'tracking_summary': [ignore_missing], '__extras': [ignore_missing, extras_unicode_convert, keep_extras], } diff --git a/ckan/model/activity.py b/ckan/model/activity.py index 73d5779622d..5ab31a27514 100644 --- a/ckan/model/activity.py +++ b/ckan/model/activity.py @@ -186,20 +186,32 @@ def group_activity_list(group_id, limit, offset): def _activites_from_users_followed_by_user_query(user_id): '''Return a query for all activities from users that user_id follows.''' import ckan.model as model - q = model.Session.query(model.Activity) - q = q.join(model.UserFollowingUser, - model.UserFollowingUser.object_id == model.Activity.user_id) - q = q.filter(model.UserFollowingUser.follower_id == user_id) + + # Get a list of the users that the given user is following. + follower_objects = model.UserFollowingUser.followee_list(user_id) + if not follower_objects: + # Return a query with no results. + return model.Session.query(model.Activity).filter("0=1") + + q = _user_activity_query(follower_objects[0].object_id) + q = q.union_all(*[_user_activity_query(follower.object_id) + for follower in follower_objects[1:]]) return q def _activities_from_datasets_followed_by_user_query(user_id): '''Return a query for all activities from datasets that user_id follows.''' import ckan.model as model - q = model.Session.query(model.Activity) - q = q.join(model.UserFollowingDataset, - model.UserFollowingDataset.object_id == model.Activity.object_id) - q = q.filter(model.UserFollowingDataset.follower_id == user_id) + + # Get a list of the datasets that the user is following. + follower_objects = model.UserFollowingDataset.followee_list(user_id) + if not follower_objects: + # Return a query with no results. + return model.Session.query(model.Activity).filter("0=1") + + q = _package_activity_query(follower_objects[0].object_id) + q = q.union_all(*[_package_activity_query(follower.object_id) + for follower in follower_objects[1:]]) return q diff --git a/ckan/model/meta.py b/ckan/model/meta.py index b8d6b42cd65..ce175d309d7 100644 --- a/ckan/model/meta.py +++ b/ckan/model/meta.py @@ -161,4 +161,6 @@ def engine_is_sqlite(): def engine_is_pg(): # Returns true iff the engine is connected to a postgresql database. - return engine.url.drivername in ['psycopg2', 'postgres'] + # According to http://docs.sqlalchemy.org/en/latest/core/engines.html#postgresql + # all Postgres driver names start with `postgresql` + return engine.url.drivername.startswith('postgresql') diff --git a/ckan/plugins/core.py b/ckan/plugins/core.py index 1fee98a10f3..f2ff584a63f 100644 --- a/ckan/plugins/core.py +++ b/ckan/plugins/core.py @@ -11,7 +11,7 @@ from pyutilib.component.core import SingletonPlugin as _pca_SingletonPlugin from pyutilib.component.core import Plugin as _pca_Plugin -from ckan.plugins.interfaces import IPluginObserver +from ckan.plugins.interfaces import IPluginObserver, IGenshiStreamFilter __all__ = [ 'PluginImplementations', 'implements', @@ -123,6 +123,10 @@ def load(plugin): service.activate() for observer_plugin in observers: observer_plugin.after_load(service) + + if IGenshiStreamFilter in service.__interfaces__: + log.warn("Plugin '%s' is using deprecated interface IGenshiStreamFilter" % plugin) + return service diff --git a/ckan/public/base/css/main.css b/ckan/public/base/css/main.css index 6d4fc938097..66aab31b86b 100644 --- a/ckan/public/base/css/main.css +++ b/ckan/public/base/css/main.css @@ -4506,13 +4506,13 @@ ul.icons li .icon-large:before { .simple-list:after { clear: both; } -.simple-list > li { +.simple-list > li { font-size: 12px; line-height: 1.1666666666666667em; padding: 7px 25px; border-bottom: 1px dotted #cccccc; } -.simple-list > li:last-of-type { +.simple-list > li:last-of-type { border-bottom: 0; } .simple-list .ckan-icon { @@ -4669,8 +4669,6 @@ ul.icons li .icon-large:before { padding-right: 15px; } .module-grid { - margin: 0; - list-style: none; margin: 0; list-style: none; *zoom: 1; @@ -4694,7 +4692,6 @@ ul.icons li .icon-large:before { padding-left: 20px; padding-bottom: 25px; float: left; - float: left; margin-left: 20px; width: 460px; padding-top: 10px; @@ -4720,10 +4717,10 @@ ul.icons li .icon-large:before { .ckanext-datapreview { position: relative; } -.ckanext-datapreview > iframe { +.ckanext-datapreview > iframe { min-height: 400px; } -.ckanext-datapreview > img { +.ckanext-datapreview > img { max-height: 500px; max-width: 100%; overflow: hidden; @@ -4887,13 +4884,13 @@ ol.media-grid:after { .nav-simple:after { clear: both; } -.nav-simple > li { +.nav-simple > li { font-size: 12px; line-height: 1.1666666666666667em; padding: 7px 25px; border-bottom: 1px dotted #cccccc; } -.nav-simple > li:last-of-type { +.nav-simple > li:last-of-type { border-bottom: 0; } .nav-simple .ckan-icon { @@ -4915,12 +4912,10 @@ ol.media-grid:after { } .nav-item.active > a { background: url("../../../base/images/background-tag.png") no-repeat -13px center; - position: relative; display: block; font-size: 11px; line-height: 27px; color: #187794; - padding-left: 10px; padding-right: 5px; margin-right: 11px; -webkit-box-sizing: border-box; @@ -5226,8 +5221,6 @@ textarea { background-image: url("../../../base/images/sprite-ckan-icons.png"); background-repeat: no-repeat; background-position: 16px 16px; - width: 17px; - height: 17px; background-position: -51px -16px; position: absolute; display: block; @@ -5654,6 +5647,13 @@ textarea { .control-full .select2-container { width: 520px !important; } +.control-group.error .select2-container input:focus, +.control-group.error .select2-container select:focus, +.control-group.error .select2-container textarea:focus { + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} .dataset-item { border-bottom: 1px dotted #cccccc; padding-bottom: 20px; @@ -5821,8 +5821,6 @@ textarea { vertical-align: text-bottom; position: relative; top: 2px; - width: 16px; - height: 16px; background-image: url("../../../base/images/sprite-ckan-icons.png"); background-repeat: no-repeat; background-position: 16px 16px; @@ -6682,7 +6680,6 @@ li .icon-large:before { float: right; } [role=main] .secondary { - float: left; margin-left: 20px; width: 220px; margin-left: 0; @@ -6993,6 +6990,9 @@ header.masthead .account.not-authed { header.masthead .dropdown-menu { margin-top: -1px; } +header.masthead .user-dropdown-menu { + min-width: 120px; +} header.masthead .user-dropdown-menu a { color: #005d7a; } @@ -7239,9 +7239,18 @@ header.masthead .debug { display: block; font-size: 30px; font-weight: 700; - line-height: 1.2; + line-height: 36px; margin-left: 0; } +.profile-info .nums dl dd .smallest { + font-size: 13px; +} +.profile-info .nums dl dd .smaller { + font-size: 16px; +} +.profile-info .nums dl dd .small { + font-size: 21px; +} .profile-info.editing .module-content { margin-top: 0; } @@ -7430,9 +7439,175 @@ header.masthead .debug { -moz-border-radius: 100px; border-radius: 100px; } -.module-my-datasets .empty { +.dashboard .module-content { + *zoom: 1; + background: transparent url("../../../base/images/full-width-nav-right.png") 100% 0 repeat-y; +} +.dashboard .module-content:before, +.dashboard .module-content:after { + display: table; + content: ""; +} +.dashboard .module-content:after { + clear: both; +} +.dashboard .dashboard-main { + float: left; + width: 630px; + margin-right: 20px; +} +.dashboard .dashboard-aside { + float: left; + width: 220px; + padding-left: 20px; +} +.dashboard .dashboard-aside .nav-list { + margin: 10px -25px 0 -20px; +} +.dashboard .dashboard-aside .nav-list li a { + padding: 5px 20px; +} +#followee-filter .btn { + *zoom: 1; +} +#followee-filter .btn:before, +#followee-filter .btn:after { + display: table; + content: ""; +} +#followee-filter .btn:after { + clear: both; +} +#followee-filter .btn span, +#followee-filter .btn strong { + display: block; + float: left; + line-height: 1.5; +} +#followee-filter .btn span { + font-weight: normal; +} +#followee-filter .btn strong { + margin: 0 5px; + white-space: nowrap; + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; +} +.dashboard-aside-context { + position: relative; + margin: -20px -25px 20px -20px; + padding: 20px; + border-left: 1px solid #DCDCDC; + border-bottom: 1px solid #DCDCDC; + background-color: #f5f5f5; + -webkit-border-radius: 0 5px 0 0; + -moz-border-radius: 0 5px 0 0; + border-radius: 0 5px 0 0; +} +.dashboard-aside-context h2 { + margin-bottom: 10px; +} +.dashboard-aside-context .arrow { + position: absolute; + content: ' '; + top: 24px; + left: -10px; + width: 10px; + height: 21px; + background: transparent url("../../../base/images/dashboard-followee-related.png"); +} +.popover-followee .popover-title { + display: none; +} +.popover-followee .popover-content { + padding: 0; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +.popover-followee .empty { padding: 10px; } +.popover-followee .popover-header { + *zoom: 1; + background-color: whiteSmoke; + padding: 5px; + border-bottom: 1px solid #cccccc; + -webkit-border-radius: 3px 3px 0 0; + -moz-border-radius: 3px 3px 0 0; + border-radius: 3px 3px 0 0; +} +.popover-followee .popover-header:before, +.popover-followee .popover-header:after { + display: table; + content: ""; +} +.popover-followee .popover-header:after { + clear: both; +} +.popover-followee .popover-header .add-on, +.popover-followee .popover-header input { + float: left; + margin: 0; +} +.popover-followee .popover-header .add-on { + padding: 4px 8px 4px 12px; + border-right-width: 0; + -webkit-border-radius: 100px 0 0 100px; + -moz-border-radius: 100px 0 0 100px; + border-radius: 100px 0 0 100px; +} +.popover-followee .popover-header input { + padding: 4px 12px 4px 8px; + font-size: 13px; + width: 211px; + -webkit-border-radius: 0 100px 100px 0; + -moz-border-radius: 0 100px 100px 0; + border-radius: 0 100px 100px 0; +} +.popover-followee .nav { + padding: 0; + max-height: 205px; + overflow: auto; + -webkit-border-radius: 0 0 3px 3px; + -moz-border-radius: 0 0 3px 3px; + border-radius: 0 0 3px 3px; +} +.popover-followee .nav li a { + display: block; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + padding: 7px 10px 7px 15px; + margin: 0; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +.popover-followee .nav li a i { + background-color: #187794; + color: #ffffff; + margin-right: 11px; + padding: 3px 5px; + line-height: 1; + -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); +} +.popover-followee .nav li a:hover i { + background-color: #000; +} +.popover-followee .nav li.active a i { + color: #187794; + background-color: #ffffff; +} +.ie8 .dashboard-aside { + width: 218px; +} .dropdown:hover .dropdown-menu { display: block; } diff --git a/ckan/public/base/images/dashboard-followee-related.png b/ckan/public/base/images/dashboard-followee-related.png new file mode 100644 index 00000000000..81374b09273 Binary files /dev/null and b/ckan/public/base/images/dashboard-followee-related.png differ diff --git a/ckan/public/base/images/full-width-nav-right.png b/ckan/public/base/images/full-width-nav-right.png new file mode 100644 index 00000000000..344b92cb458 Binary files /dev/null and b/ckan/public/base/images/full-width-nav-right.png differ diff --git a/ckan/public/base/javascript/modules/dashboard.js b/ckan/public/base/javascript/modules/dashboard.js index 7c30c655c6c..263b3467ec9 100644 --- a/ckan/public/base/javascript/modules/dashboard.js +++ b/ckan/public/base/javascript/modules/dashboard.js @@ -1,11 +1,93 @@ +/* User Dashboard + * Handles the filter dropdown menu and the reduction of the notifications number + * within the header to zero + * + * Examples + * + *
+ * + */ this.ckan.module('dashboard', function ($, _) { - return { - initialize: function () { - if ($('.new', this.el)) { - setTimeout(function() { - $('.masthead .notifications').removeClass('notifications-important').html('0'); - }, 1000); - } - } - }; + return { + button: null, + popover: null, + searchTimeout: null, + + /* Initialises the module setting up elements and event listeners. + * + * Returns nothing. + */ + initialize: function () { + $.proxyAll(this, /_on/); + this.button = $('#followee-filter .btn'). + on('click', this._onShowFolloweeDropdown); + var title = this.button.prop('title'); + this.button.popover({ + placement: 'bottom', + title: 'Filter', + html: true, + content: $('#followee-popover').html() + }); + this.button.prop('title', title); + this.popover = this.button.data('popover').tip().addClass('popover-followee'); + // Are there new items in the dashboard? If so... reset the + // notifications number to zero + if ($('.new', this.el)) { + setTimeout(function() { + $('.masthead .notifications').removeClass('notifications-important').html('0'); + }, 2000); + } + }, + + /* Handles click event on the 'show me:' dropdown button + * + * Returns nothing. + */ + _onShowFolloweeDropdown: function() { + this.button.toggleClass('active'); + if (this.button.hasClass('active')) { + setTimeout(this._onInitSearch, 100); + } + return false; + }, + + /* Handles focusing on the input and making sure that the keyup + * even is applied to the input + * + * Returns nothing. + */ + _onInitSearch: function() { + var input = $('input', this.popover); + if (!input.hasClass('inited')) { + input. + on('keyup', this._onSearchKeyUp). + addClass('inited'); + } + input.focus(); + }, + + /* Handles the keyup event + * + * Returns nothing. + */ + _onSearchKeyUp: function() { + clearTimeout(this.searchTimeout); + this.searchTimeout = setTimeout(this._onSearchKeyUpTimeout, 300); + }, + + /* Handles the actual filtering of search results + * + * Returns nothing. + */ + _onSearchKeyUpTimeout: function() { + var input = $('input', this.popover); + var q = input.val().toLowerCase(); + if (q) { + $('li', this.popover).hide(); + $('li.everything, [data-search^="' + q + '"]', this.popover).show(); + } else { + $('li', this.popover).show(); + } + } + }; }); diff --git a/ckan/public/base/javascript/modules/data-viewer.js b/ckan/public/base/javascript/modules/data-viewer.js index f928ed1c0d3..5b9cbafce98 100644 --- a/ckan/public/base/javascript/modules/data-viewer.js +++ b/ckan/public/base/javascript/modules/data-viewer.js @@ -12,6 +12,14 @@ this.ckan.module('data-viewer', function (jQuery) { jQuery.proxyAll(this, /_on/); this.el.on('load', this._onLoad); this._FirefoxFix(); + this.sandbox.subscribe('data-viewer-error', this._onDataViewerError); + }, + + _onDataViewerError: function(message) { + var parent = this.el.parent(); + $('.data-viewer-error .collapse', parent).html(message); + $('.data-viewer-error', parent).removeClass('js-hide'); + this.el.hide(); }, _onLoad: function() { diff --git a/ckan/public/base/javascript/modules/popover-context.js b/ckan/public/base/javascript/modules/popover-context.js index 31651eef06e..18a118709af 100644 --- a/ckan/public/base/javascript/modules/popover-context.js +++ b/ckan/public/base/javascript/modules/popover-context.js @@ -1,7 +1,7 @@ /* Popover context * These appear when someone hovers over a context item in a activity stream to - * give the user more context into that particular item. It also allows for people to - * follow and unfollow quickly from within the popover + * give the user more context into that particular item. It also allows for + * people to follow and unfollow quickly from within the popover * * id - The user_id of user * context - The type of this popover: currently supports user & package @@ -100,7 +100,8 @@ this.ckan.module('popover-context', function($, _) { } }, - /* Handles the showing of the popover on hover (also hides other active popovers) + /* Handles the showing of the popover on hover (also hides other active + * popovers) * * Returns nothing. */ @@ -162,7 +163,8 @@ this.ckan.module('popover-context', function($, _) { } }, - /* Used to break down a raw object into something a little more passable into a GET request + /* Used to break down a raw object into something a little more + * passable into a GET request * * Returns object. */ @@ -183,14 +185,14 @@ this.ckan.module('popover-context', function($, _) { params.title = raw.title; params.name = raw.name; params.notes = raw.notes; - params.num_resources = raw.resources.length; - params.num_tags = raw.tags.length; + params.num_resources = raw.num_resources; + params.num_tags = raw.num_tags; } else if (type == 'group') { params.id = raw.id; params.title = raw.title; params.name = raw.name; params.description = raw.description; - params.num_datasets = raw.packages.length; + params.package_count = raw.package_count; params.num_followers = raw.num_followers; } return params; @@ -244,8 +246,8 @@ this.ckan.module('popover-context', function($, _) { return false; }, - /* Callback from when you follow/unfollow a specified item... this is used to ensure - * all popovers associated to that user get re-populated + /* Callback from when you follow/unfollow a specified item... this is + * used to ensure all popovers associated to that user get re-populated * * Returns nothing. */ diff --git a/ckan/public/base/less/dashboard.less b/ckan/public/base/less/dashboard.less index 63223bbcd7b..34c524ffadb 100644 --- a/ckan/public/base/less/dashboard.less +++ b/ckan/public/base/less/dashboard.less @@ -1,5 +1,140 @@ -.module-my-datasets { +.dashboard { + .module-content { + .clearfix(); + background: transparent url("@{imagePath}/full-width-nav-right.png") 100% 0 repeat-y; + } + .dashboard-main { + float: left; + width: 630px; + margin-right: 20px; + } + .dashboard-aside { + float: left; + width: 220px; + padding-left: 20px; + .nav-list { + margin: 10px -25px 0 -20px; + li a { + padding: 5px 20px; + } + } + } +} + +#followee-filter { + .btn { + .clearfix(); + span, strong { + display: block; + float: left; + line-height: 1.5; + } + span { + font-weight: normal; + } + strong { + margin: 0 5px; + white-space: nowrap; + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; + } + } +} + +.dashboard-aside-context { + position: relative; + margin: -20px -25px 20px -20px; + padding: 20px; + border-left: 1px solid #DCDCDC; + border-bottom: 1px solid #DCDCDC; + background-color: @moduleHeadingBackgroundColorStart; + .border-radius(0 5px 0 0); + h2 { + margin-bottom: 10px; + } + .arrow { + position: absolute; + content: ' '; + top: 24px; + left: -10px; + width: 10px; + height: 21px; + background: transparent url("@{imagePath}/dashboard-followee-related.png"); + } +} + +.popover-followee { + .popover-title { + display: none; + } + .popover-content { + padding: 0; + .border-radius(3px); + } .empty { padding: 10px; } + .popover-header { + .clearfix(); + background-color: whiteSmoke; + padding: 5px; + border-bottom: 1px solid @inputBorder; + .border-radius(3px 3px 0 0); + .add-on, + input { + float: left; + margin: 0; + } + .add-on { + padding: 4px 8px 4px 12px; + border-right-width: 0; + .border-radius(100px 0 0 100px); + } + input { + padding: 4px 12px 4px 8px; + font-size: 13px; + width: 211px; + .border-radius(0 100px 100px 0); + } + } + .nav { + padding: 0; + max-height: 205px; + overflow: auto; + .border-radius(0 0 3px 3px); + li { + a { + display: block; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + padding: 7px 10px 7px 15px; + margin: 0; + .border-radius(0); + i { + background-color: @layoutLinkColor; + color: @inputBackground; + margin-right: 11px; + padding: 3px 5px; + line-height: 1; + .border-radius(100px); + .box-shadow(inset 0 1px 2x rgba(0, 0, 0, 0.2)); + } + &:hover i { + background-color: #000; + } + } + &.active a i { + color: @layoutLinkColor; + background-color: @inputBackground; + } + } + } +} + +.ie8 { + .dashboard-aside { + width: 218px; + } } diff --git a/ckan/public/base/less/forms.less b/ckan/public/base/less/forms.less index 445c2677ec2..5902a859f8b 100644 --- a/ckan/public/base/less/forms.less +++ b/ckan/public/base/less/forms.less @@ -686,3 +686,11 @@ textarea { // sets it on the element. width: 520px !important; } + +.control-group.error .select2-container { + input:focus, + select:focus, + textarea:focus { + .box-shadow(none); + } +} diff --git a/ckan/public/base/less/masthead.less b/ckan/public/base/less/masthead.less index 31397c72e2d..a4e6eadd27e 100644 --- a/ckan/public/base/less/masthead.less +++ b/ckan/public/base/less/masthead.less @@ -184,6 +184,7 @@ header.masthead { } .user-dropdown-menu { + min-width: 120px; a { color: @mastheadBackgroundColorStart; &:hover { diff --git a/ckan/templates/activity_streams/activity_stream_items.html b/ckan/templates/activity_streams/activity_stream_items.html index 61a557af941..ff863425a5a 100644 --- a/ckan/templates/activity_streams/activity_stream_items.html +++ b/ckan/templates/activity_streams/activity_stream_items.html @@ -1,16 +1,20 @@ {% set has_more_length = g.activity_list_limit|int %} {% set has_more = activities|length > has_more_length %} - +{% if activities %} + +{% else %} +

{{ _('No activities are within this activity stream') }}

+{% endif %} diff --git a/ckan/templates/admin/authz.html b/ckan/templates/admin/authz.html index cecb18a9f75..5b60f95cc27 100644 --- a/ckan/templates/admin/authz.html +++ b/ckan/templates/admin/authz.html @@ -14,9 +14,9 @@ {% endfor %} - {{ _('User') }} + {{ _('User') }} {% for role in roles %} - {{ role }} + {{ role }} {% endfor %} {% for user in users %} @@ -49,9 +49,9 @@ {% endfor %} - {{ _('User') }} + {{ _('User') }} {% for role in roles %} - {{ role }} + {{ role }} {% endfor %} @@ -77,9 +77,9 @@ {% endfor %} - {{ _('User Group') }} + {{ _('User Group') }} {% for role in roles %} - {{ role }} + {{ role }} {% endfor %} {% for user in users %} @@ -112,9 +112,9 @@ {% endfor %} - User Group + User Group {% for role in roles %} - {{ role }} + {{ role }} {% endfor %} diff --git a/ckan/templates/ajax_snippets/api_info.html b/ckan/templates/ajax_snippets/api_info.html index f339054e86d..04888329c42 100644 --- a/ckan/templates/ajax_snippets/api_info.html +++ b/ckan/templates/ajax_snippets/api_info.html @@ -19,7 +19,7 @@

CKAN Data API

- +

Access resource data via a web API with powerful query support. Further information in the main @@ -39,19 +39,19 @@

- Create + Create {{ datastore_root_url }}/datastore_create - Update / Insert + Update / Insert {{ datastore_root_url }}/datastore_upsert - Query + Query {{ datastore_root_url }}/datastore_search - Query (via SQL) + Query (via SQL) {{ datastore_root_url }}/datastore_search_sql diff --git a/ckan/templates/ajax_snippets/popover_context_group.html b/ckan/templates/ajax_snippets/popover_context_group.html index 75a1b10f83c..62255d12d10 100644 --- a/ckan/templates/ajax_snippets/popover_context_group.html +++ b/ckan/templates/ajax_snippets/popover_context_group.html @@ -19,7 +19,7 @@
{{ _('Datasets') }}
-
{{ h.SI_number_span(num_datasets) }}
+
{{ h.SI_number_span(package_count) }}
diff --git a/ckan/templates/dataviewer/snippets/data_preview.html b/ckan/templates/dataviewer/snippets/data_preview.html index 48ce3b16e1c..d77593d6e04 100644 --- a/ckan/templates/dataviewer/snippets/data_preview.html +++ b/ckan/templates/dataviewer/snippets/data_preview.html @@ -3,8 +3,24 @@ {# images can be embedded directly #} {% else %} - +
+ {% endif %} - \ No newline at end of file + diff --git a/ckan/templates/group/read_base.html b/ckan/templates/group/read_base.html index e41cb7ebe7d..a16615b7777 100644 --- a/ckan/templates/group/read_base.html +++ b/ckan/templates/group/read_base.html @@ -26,9 +26,6 @@ {% link_for _('Activity Stream'), controller='group', action='activity', id=c.group_dict.name, icon='time' %} - - {% link_for _('Administrators'), controller='group', action='admins', id=c.group_dict.name, icon='cog' %} - {% link_for _('About'), controller='group', action='about', id=c.group_dict.name, icon='info-sign' %} diff --git a/ckan/templates/home/index.html b/ckan/templates/home/index.html index 763dd06646c..153119917a9 100644 --- a/ckan/templates/home/index.html +++ b/ckan/templates/home/index.html @@ -69,7 +69,7 @@

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

{% block home_tags %}
-

Popular {{ c.facet_titles.name }}

+

{{ _('Popular Tags') }}

{% set tags = h.get_facet_items_dict('tags', limit=3) %} {% snippet 'snippets/tag_list.html', tags=tags %}
diff --git a/ckan/templates/organization/members.html b/ckan/templates/organization/members.html index fd0e34b6496..175af5e7566 100644 --- a/ckan/templates/organization/members.html +++ b/ckan/templates/organization/members.html @@ -11,9 +11,9 @@

{{ _('Members') }}

- {{ _('User') }} - {{ _('Role') }} - + {{ _('User') }} + {{ _('Role') }} + diff --git a/ckan/templates/organization/read_base.html b/ckan/templates/organization/read_base.html index d0c142f015c..23dc5c71ccd 100644 --- a/ckan/templates/organization/read_base.html +++ b/ckan/templates/organization/read_base.html @@ -33,7 +33,7 @@ {% endblock %} {% block secondary_content %} - {% snippet 'snippets/organization.html', organization=c.group_dict %} + {% snippet 'snippets/organization.html', organization=c.group_dict, show_nums=true %} {% if h.user_in_org_or_group(c.group_dict.id) %} {{ h.snippet('snippets/facet_list.html', title='Visibility', name='capacity', extras={'id':c.group_dict.id}) }} diff --git a/ckan/templates/package/read_base.html b/ckan/templates/package/read_base.html index 14c90e7cbf9..9f457eae772 100644 --- a/ckan/templates/package/read_base.html +++ b/ckan/templates/package/read_base.html @@ -55,6 +55,12 @@ {% block secondary_help_content %}{% endblock %} + {% block package_organization %} + {% if pkg.organization %} + {% snippet "snippets/organization.html", organization=pkg.organization, truncate=70, show_nums=false %} + {% endif %} + {% endblock %} + {% block package_groups %} {% for group in pkg.groups %} {% snippet "snippets/group.html", group=group, truncate=70 %} diff --git a/ckan/templates/package/resource_read.html b/ckan/templates/package/resource_read.html index 0f6f5c6d849..32dbcfc0a03 100644 --- a/ckan/templates/package/resource_read.html +++ b/ckan/templates/package/resource_read.html @@ -79,13 +79,13 @@

Additional Information

- - + + {% for key, value in h.format_resource_items(res.items()) %} - + {% endfor %}
FieldValueFieldValue
{{ key }}{{ value }}
{{ key }}{{ value }}
diff --git a/ckan/templates/package/snippets/additional_info.html b/ckan/templates/package/snippets/additional_info.html index 82e1eabd7d8..64821f08f59 100644 --- a/ckan/templates/package/snippets/additional_info.html +++ b/ckan/templates/package/snippets/additional_info.html @@ -3,52 +3,52 @@

{{ _('Additional Info') }}

- - + + {% if pkg_dict.url %} - + {% endif %} {% if pkg_dict.author_email %} - + {% elif pkg_dict.author %} - + {% endif %} {% if pkg_dict.maintainer_email %} - + {% elif pkg_dict.maintainer %} - + {% endif %} {% if pkg_dict.version %} - + {% endif %} {% if h.check_access('package_update',{'id':pkg_dict.id}) %} - + {% endif %} @@ -56,7 +56,7 @@

{{ _('Additional Info') }}

{% for extra in h.sorted_extras(pkg_dict.extras) %} {% set key, value = extra %} - + {% endfor %} diff --git a/ckan/templates/snippets/organization.html b/ckan/templates/snippets/organization.html index d0fddf914bd..8bbd4a9f418 100644 --- a/ckan/templates/snippets/organization.html +++ b/ckan/templates/snippets/organization.html @@ -26,18 +26,20 @@

{{ organization.title or organization.name }}

{% link_for _('read more'), controller='organization', action='about', id=organization.name %}

{% else %} -

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

+

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

+ {% endif %} + {% if show_nums %} +
+
+
{{ _('Members') }}
+
{{ h.SI_number_span(organization.members|length) }}
+
+
+
{{ _('Datasets') }}
+
{{ h.SI_number_span(organization.package_count) }}
+
+
{% endif %} -
-
-
{{ _('Members') }}
-
{{ h.SI_number_span(organization.members|length) }}
-
-
-
{{ _('Datasets') }}
-
{{ h.SI_number_span(organization.package_count) }}
-
-
{% endwith %} diff --git a/ckan/templates/snippets/popover_context.html b/ckan/templates/snippets/popover_context.html new file mode 100644 index 00000000000..d445d5b6162 --- /dev/null +++ b/ckan/templates/snippets/popover_context.html @@ -0,0 +1,10 @@ +{%if type == 'user' %} +

{{ dict.name }}

+ {% snippet 'ajax_snippets/popover_context_user.html', id=dict.id, name=dict.name, about=dict.about, is_me='false', num_followers=dict.num_followers, number_administered_packages=dict.number_administered_packages, number_of_edits=dict.number_of_edits %} +{%elif type == 'dataset' %} +

{{ dict.title }}

+ {% snippet 'ajax_snippets/popover_context_dataset.html', id=dict.id, name=dict.name, notes=dict.notes, num_resources=dict.num_resources, num_tags=dict.num_tags %} +{%elif type == 'group' %} +

{{ dict.title }}

+ {% snippet 'ajax_snippets/popover_context_group.html', id=dict.id, name=dict.name, description=dict.description, num_followers=dict.num_followers, package_count=dict.package_count %} +{% endif %} diff --git a/ckan/templates/snippets/related.html b/ckan/templates/snippets/related.html index 4a4395cc2e3..380a6276709 100644 --- a/ckan/templates/snippets/related.html +++ b/ckan/templates/snippets/related.html @@ -1,5 +1,5 @@
-

Related Add Related

+

Related

{% if item %} {% with url = h.url_for(controller='related', action='list', id=pkg_name) %} @@ -12,9 +12,11 @@

{{ item.title }}

{% endwith %} {% else %} -

No apps, ideas, news stories or images have been - related to this dataset yet, why not add one now?

-

{% link_for _('Add Item'), controller='related', action='new', id=pkg_name, icon='plus', class_='btn' %}

+

{% trans %}No apps, ideas, news stories or images have been + related to this dataset yet.{% endtrans %}

+ {% if h.check_access('related_create') %} +

{% link_for _('Add Item'), controller='related', action='new', id=pkg_name, icon='plus', class_='btn' %}

+ {% endif %} {% endif %}
diff --git a/ckan/templates/user/dashboard.html b/ckan/templates/user/dashboard.html index 2beaaaddea9..40b2a9387f1 100644 --- a/ckan/templates/user/dashboard.html +++ b/ckan/templates/user/dashboard.html @@ -5,40 +5,49 @@ {% block subtitle %}{{ _('Dashboard') }}{% endblock %} {% block breadcrumb_content %} -
  • {{ _('Dashboard') }}
  • +
  • {{ _('Dashboard') }}
  • {% endblock %} {% block actions_content %} {% if h.check_access('package_create') %} -
  • {% link_for _('Add Dataset'), controller='package', action='new', class_="btn btn-primary icon-large", icon="plus" %}
  • +
  • {% link_for _('Add a dataset'), controller='package', action='new', class_="btn btn-primary icon-large", icon="plus" %}
  • {% endif %} {% endblock %} -{% block primary_content %} -
    -
    -

    {{ _('News feed') }} {{ _('Activity from users and datasets that you follow') }}

    - {{ c.dashboard_activity_stream }} +{% block primary %} +
    +
    +
    + {% snippet 'user/snippets/followee_dropdown.html', context=c.dashboard_activity_stream_context, followees=c.followee_list %} +

    + {{ _('News feed') }} + {{ _('Activity from items that you follow') }} +

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

    {{ _('My Datasets') }}

    - {% if c.user_dict['datasets'] %} - - {% else %} -

    {{ _('You currently have not added any datasets yet') }}

    - {% endif %} -
    -{% endblock %} +{% block sidebar %}{% endblock %} diff --git a/ckan/templates/user/read.html b/ckan/templates/user/read.html index 9a272ecfa3e..c2afb1bca6c 100644 --- a/ckan/templates/user/read.html +++ b/ckan/templates/user/read.html @@ -11,7 +11,9 @@

    {{ _('Datasets') }}

    {% if c.is_myself %}

    {{ _('You haven\'t created any datasets.') }} - {% link_for _('Create one now?'), controller='package', action='new' %}. + {% if h.check_access('package_create') %} + {% link_for _('Create one now?'), controller='package', action='new' %} + {% endif %}

    {% else %}

    @@ -20,4 +22,4 @@

    {{ _('Datasets') }}

    {% endif %} {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/ckan/templates/user/snippets/followee_dropdown.html b/ckan/templates/user/snippets/followee_dropdown.html new file mode 100644 index 00000000000..c3daffbce64 --- /dev/null +++ b/ckan/templates/user/snippets/followee_dropdown.html @@ -0,0 +1,45 @@ +{% macro followee_icon(type) -%} + {% if type == 'dataset' %} + + {% elif type == 'user' %} + + {% elif type == 'group' %} + + {% endif %} +{%- endmacro %} + +
    + + {{ _('Activity from:') }} + {{ context.context }} + + + +
    +
    + + +
    +
    + {% if followees %} + + {% else %} +

    {{ _('You are not following anything') }}

    + {% endif %} + +
    diff --git a/ckan/tests/functional/api/test_activity.py b/ckan/tests/functional/api/test_activity.py index 51bee6de429..e5473df8d4c 100644 --- a/ckan/tests/functional/api/test_activity.py +++ b/ckan/tests/functional/api/test_activity.py @@ -17,7 +17,6 @@ from ckan.lib.helpers import json -##<<<<<<< HEAD ##def package_update(context, data_dict): ## # These tests call package_update directly which is really bad ## # setting api_version in context make things seem like the api key @@ -33,7 +32,6 @@ ## context['api_version'] = 3 ## context['ignore_auth'] = True ## return _package_create(context, data_dict) -##======= def package_show(app, data_dict, apikey=None): if apikey: extra_environ = {'Authorization': str(apikey)} @@ -2085,10 +2083,43 @@ def test_follow_user(self): #if 'id' not in activity: # assert False, "activity object should have an id value" # TODO: Test for the _correct_ revision_id value. + #if 'revision_id' not in activity: # assert False, "activity object should have a revision_id value" #timestamp = datetime_from_string(activity['timestamp']) #assert timestamp >= before['time'] and timestamp <= \ # after['time'], str(activity['timestamp']) - # + #assert len(self.activity_details(activity)) == 0 + + def test_user_activity_list_by_name(self): + '''user_activity_list should accept a user name as param.''' + import ckan.tests + activities = ckan.tests.call_action_api(self.app, 'user_activity_list', + id='annafan') + assert len(activities) > 0 + + def test_package_activity_list_by_name(self): + '''package_activity_list should accept a package name as param.''' + import ckan.tests + activities = ckan.tests.call_action_api(self.app, + 'package_activity_list', id='warandpeace', + apikey=self.sysadmin_user['apikey']) + assert len(activities) > 0 + + def test_group_activity_list_by_name(self): + '''group_activity_list should accept a group name as param.''' + import ckan.tests + activities = ckan.tests.call_action_api(self.app, + 'group_activity_list', id='roger') + assert len(activities) > 0 + + def test_organization_activity_list_by_name(self): + '''organization_activity_list should accept a org name as param.''' + import ckan.tests + organization = ckan.tests.call_action_api(self.app, + 'organization_create', name='test_org', + apikey=self.sysadmin_user['apikey']) + activities = ckan.tests.call_action_api(self.app, + 'organization_activity_list', id=organization['name']) + assert len(activities) > 0 diff --git a/ckan/tests/functional/api/test_follow.py b/ckan/tests/functional/api/test_follow.py index eb4530e3cea..79e6f5dab17 100644 --- a/ckan/tests/functional/api/test_follow.py +++ b/ckan/tests/functional/api/test_follow.py @@ -25,6 +25,43 @@ def datetime_from_string(s): ''' return datetime.datetime.strptime(s, '%Y-%m-%dT%H:%M:%S.%f') +def follow(func): + '''Return a wrapper function for a follow_* function. + + The wrapper functions test the `followee_list` and `followee_count` API + calls, in addition to any tests carried out by the wrapped function. + + ''' + def wrapped_func(app, follower_id, apikey, object_id, object_arg, + sysadmin_apikey): + followee_count_before = ckan.tests.call_action_api(app, + 'followee_count', id=follower_id) + followees_before = ckan.tests.call_action_api(app, 'followee_list', + id=follower_id, apikey=sysadmin_apikey) + + func(app, follower_id, apikey, object_id, object_arg, sysadmin_apikey) + + followee_count_after = ckan.tests.call_action_api(app, + 'followee_count', id=follower_id) + followees_after = ckan.tests.call_action_api(app, 'followee_list', + id=follower_id, apikey=sysadmin_apikey) + + assert followee_count_after == followee_count_before + 1, ( + "After a user follows an object, the user's `followee_count` " + "should increase by 1") + + assert len(followees_after) == len(followees_before) + 1, ( + "After a user follows an object, the object should appear in " + "the user's `followee_list`") + assert len([followee for followee in followees_after + if followee['dict']['id'] == object_id]) == 1, ( + "After a user follows an object, the object should appear in " + "the user's `followee_list`") + + return wrapped_func + + +@follow def follow_user(app, follower_id, apikey, object_id, object_arg, sysadmin_apikey): '''Test a user starting to follow another user via the API. @@ -86,6 +123,8 @@ def follow_user(app, follower_id, apikey, object_id, object_arg, 'user_followee_count', id=follower_id) assert followee_count_after == followee_count_before + 1 + +@follow def follow_dataset(app, follower_id, apikey, dataset_id, dataset_arg, sysadmin_apikey): '''Test a user starting to follow a dataset via the API. @@ -147,6 +186,8 @@ def follow_dataset(app, follower_id, apikey, dataset_id, dataset_arg, 'dataset_followee_count', id=follower_id) assert followee_count_after == followee_count_before + 1 + +@follow def follow_group(app, user_id, apikey, group_id, group_arg, sysadmin_apikey): '''Test a user starting to follow a group via the API. @@ -297,6 +338,20 @@ def test_00_sysadmin_can_get_group_follower_list(self): ckan.tests.call_action_api(self.app, 'group_follower_list', id='roger', status=200, apikey=self.testsysadmin['apikey']) + def test_00_visitor_cannot_get_followee_list(self): + ckan.tests.call_action_api(self.app, 'followee_list', + id=self.russianfan['id'], status=403) + + def test_00_user_cannot_get_followee_list(self): + ckan.tests.call_action_api(self.app, 'followee_list', + id=self.russianfan['id'], status=403, + apikey=self.annafan['apikey']) + + def test_00_sysadmin_can_get_followee_list(self): + ckan.tests.call_action_api(self.app, 'followee_list', + id=self.russianfan['id'], status=200, + apikey=self.testsysadmin['apikey']) + def test_00_visitor_cannot_get_user_followee_list(self): '''A visitor cannot see what users a user is following.''' ckan.tests.call_action_api(self.app, 'user_followee_list', @@ -509,6 +564,57 @@ def test_04_group_follower_count_no_followers(self): 'group_follower_count', id=self.davids_group['id']) assert follower_count == 0 + def _followee_count_bad_id(self, action): + for object_id in ('bad id', ' ', 3, 35.7, 'xxx', ''): + error = ckan.tests.call_action_api(self.app, action, + status=409, id=object_id) + assert 'id' in error + + def test_04_followee_count_bad_id(self): + self._followee_count_bad_id('followee_count') + + def test_04_user_followee_count_bad_id(self): + self._followee_count_bad_id('user_followee_count') + + def test_04_dataset_followee_count_bad_id(self): + self._followee_count_bad_id('dataset_followee_count') + + def test_04_group_followee_count_bad_id(self): + self._followee_count_bad_id('group_followee_count') + + def _followee_count_missing_id(self, action): + error = ckan.tests.call_action_api(self.app, action, status=409) + assert error['id'] == ['Missing value'] + + def test_04_followee_count_missing_id(self): + self._followee_count_missing_id('followee_count') + + def test_04_user_followee_count_missing_id(self): + self._followee_count_missing_id('user_followee_count') + + def test_04_dataset_followee_count_missing_id(self): + self._followee_count_missing_id('dataset_followee_count') + + def test_04_group_followee_count_missing_id(self): + self._followee_count_missing_id('group_followee_count') + + def _followee_count_not_following_anything(self, action): + followee_count = ckan.tests.call_action_api(self.app, action, + id=self.russianfan['id']) + assert followee_count == 0 + + def test_04_followee_count_not_following_anything(self): + self._followee_count_not_following_anything('followee_count') + + def test_04_user_followee_count_not_following_anything(self): + self._followee_count_not_following_anything('user_followee_count') + + def test_04_dataset_followee_count_not_following_anything(self): + self._followee_count_not_following_anything('dataset_followee_count') + + def test_04_group_followee_count_not_following_anything(self): + self._followee_count_not_following_anything('group_followee_count') + def test_04_follower_list_bad_id(self): for action in ('user_follower_list', 'dataset_follower_list', 'group_follower_list'): @@ -541,6 +647,59 @@ def test_04_group_follower_list_no_followers(self): id=self.davids_group['id'], apikey=self.testsysadmin['apikey']) assert followers == [] + def _followee_list_bad_id(self, action): + for object_id in ('bad id', ' ', 3, 35.7, 'xxx', ''): + error = ckan.tests.call_action_api(self.app, action, + status=409, id=object_id, + apikey=self.testsysadmin['apikey']) + assert error['id'] + + def test_04_followee_list_bad_id(self): + self._followee_list_bad_id('followee_list') + + def test_04_user_followee_list_bad_id(self): + self._followee_list_bad_id('user_followee_list') + + def test_04_dataset_followee_list_bad_id(self): + self._followee_list_bad_id('dataset_followee_list') + + def test_04_group_followee_list_bad_id(self): + self._followee_list_bad_id('group_followee_list') + + def _followee_list_missing_id(self, action): + error = ckan.tests.call_action_api(self.app, action, status=409, + apikey=self.testsysadmin['apikey']) + assert error['id'] == ['Missing value'] + + def test_04_followee_list_missing_id(self): + self._followee_list_missing_id('followee_list') + + def test_04_user_followee_list_missing_id(self): + self._followee_list_missing_id('user_followee_list') + + def test_04_dataset_followee_missing_bad_id(self): + self._followee_list_missing_id('dataset_followee_list') + + def test_04_group_followee_missing_bad_id(self): + self._followee_list_missing_id('group_followee_list') + + def _followee_list_not_following_anything(self, action): + followees = ckan.tests.call_action_api(self.app, action, + id=self.russianfan['id'], apikey=self.russianfan['apikey']) + assert followees == [] + + def test_04_followee_list_not_following_anything(self): + self._followee_list_not_following_anything('followee_list') + + def test_04_user_followee_list_not_following_anything(self): + self._followee_list_not_following_anything('user_followee_list') + + def test_04_dataset_followee_not_following_anything(self): + self._followee_list_not_following_anything('dataset_followee_list') + + def test_04_group_followee_not_following_anything(self): + self._followee_list_not_following_anything('group_followee_list') + def test_04_am_following_bad_id(self): for action in ('am_following_dataset', 'am_following_user', 'am_following_group'): @@ -761,6 +920,10 @@ def _unfollow_user(self, follower_id, apikey, object_id, object_arg): # Record the user's number of followers before. count_before = ckan.tests.call_action_api(self.app, 'user_follower_count', id=object_id) + followee_count_before = ckan.tests.call_action_api(self.app, + 'followee_count', id=follower_id) + user_followee_count_before = ckan.tests.call_action_api(self.app, + 'user_followee_count', id=follower_id) # Check that the user is following the object. am_following = ckan.tests.call_action_api(self.app, @@ -787,6 +950,25 @@ def _unfollow_user(self, follower_id, apikey, object_id, object_arg): 'user_follower_count', id=object_id) assert count_after == count_before - 1 + # Check that the user doesn't appear in the subject's list of + # followees. + followees = ckan.tests.call_action_api(self.app, 'followee_list', + id=follower_id, apikey=apikey) + assert len([followee for followee in followees + if followee['dict']['id'] == object_id]) == 0 + followees = ckan.tests.call_action_api(self.app, 'user_followee_list', + id=follower_id, apikey=apikey) + assert len([followee for followee in followees + if followee['id'] == object_id]) == 0 + + # Check the the subject's followee cont has decreased by 1. + count_after = ckan.tests.call_action_api(self.app, 'followee_count', + id=follower_id) + assert count_after == followee_count_before - 1 + count_after = ckan.tests.call_action_api(self.app, + 'user_followee_count', id=follower_id) + assert count_after == user_followee_count_before - 1 + def _unfollow_dataset(self, user_id, apikey, dataset_id, dataset_arg): '''Test a user unfollowing a dataset via the API. @@ -800,6 +982,10 @@ def _unfollow_dataset(self, user_id, apikey, dataset_id, dataset_arg): # Record the dataset's number of followers before. count_before = ckan.tests.call_action_api(self.app, 'dataset_follower_count', id=dataset_id) + followee_count_before = ckan.tests.call_action_api(self.app, + 'followee_count', id=user_id) + dataset_followee_count_before = ckan.tests.call_action_api(self.app, + 'dataset_followee_count', id=user_id) # Check that the user is following the dataset. am_following = ckan.tests.call_action_api(self.app, @@ -828,6 +1014,25 @@ def _unfollow_dataset(self, user_id, apikey, dataset_id, dataset_arg): 'dataset_follower_count', id=dataset_id) assert count_after == count_before - 1 + # Check that the dataset doesn't appear in the user's list of + # followees. + followees = ckan.tests.call_action_api(self.app, 'followee_list', + id=user_id, apikey=apikey) + assert len([followee for followee in followees + if followee['dict']['id'] == dataset_id]) == 0 + followees = ckan.tests.call_action_api(self.app, + 'dataset_followee_list', id=user_id, apikey=apikey) + assert len([followee for followee in followees + if followee['id'] == dataset_id]) == 0 + + # Check the the user's followee count has decreased by 1. + count_after = ckan.tests.call_action_api(self.app, 'followee_count', + id=user_id) + assert count_after == followee_count_before - 1 + count_after = ckan.tests.call_action_api(self.app, + 'dataset_followee_count', id=user_id) + assert count_after == dataset_followee_count_before - 1 + def _unfollow_group(self, user_id, apikey, group_id, group_arg): '''Test a user unfollowing a group via the API. @@ -841,6 +1046,10 @@ def _unfollow_group(self, user_id, apikey, group_id, group_arg): # Record the group's number of followers before. count_before = ckan.tests.call_action_api(self.app, 'group_follower_count', id=group_id) + followee_count_before = ckan.tests.call_action_api(self.app, + 'followee_count', id=user_id) + group_followee_count_before = ckan.tests.call_action_api(self.app, + 'group_followee_count', id=user_id) # Check that the user is following the group. am_following = ckan.tests.call_action_api(self.app, @@ -868,6 +1077,26 @@ def _unfollow_group(self, user_id, apikey, group_id, group_arg): 'group_follower_count', id=group_id) assert count_after == count_before - 1 + # Check that the group doesn't appear in the user's list of + # followees. + followees = ckan.tests.call_action_api(self.app, 'followee_list', + id=user_id, apikey=apikey) + assert len([followee for followee in followees + if followee['dict']['id'] == group_id]) == 0 + followees = ckan.tests.call_action_api(self.app, + 'group_followee_list', id=user_id, + apikey=self.testsysadmin['apikey']) + assert len([followee for followee in followees + if followee['id'] == group_id]) == 0 + + # Check the the user's followee count has decreased by 1. + count_after = ckan.tests.call_action_api(self.app, 'followee_count', + id=user_id) + assert count_after == followee_count_before - 1 + count_after = ckan.tests.call_action_api(self.app, + 'group_followee_count', id=user_id) + assert count_after == group_followee_count_before - 1 + def test_02_follower_delete_by_id(self): self._unfollow_user(self.annafan['id'], self.annafan['apikey'], self.joeadmin['id'], self.joeadmin['id']) @@ -984,6 +1213,13 @@ def test_01_on_delete_cascade_api(self): status=409, id='joeadmin', apikey=self.testsysadmin['apikey']) assert 'id' in error + # It should no longer be possible to get joeadmin's followee lists. + for action in ('followee_list', 'user_followee_list', + 'dataset_followee_list', 'group_followee_list'): + error = ckan.tests.call_action_api(self.app, action, status=409, + id='joeadmin', apikey=self.testsysadmin['apikey']) + assert 'id' in error + # It should no longer be possible to get warandpeace's follower list. error = ckan.tests.call_action_api(self.app, 'dataset_follower_list', status=409, id='warandpeace', apikey=self.testsysadmin['apikey']) @@ -999,6 +1235,13 @@ def test_01_on_delete_cascade_api(self): status=409, id='joeadmin') assert 'id' in error + # It should no longer be possible to get joeadmin's followee counts. + for action in ('followee_count', 'user_followee_count', + 'dataset_followee_count', 'group_followee_count'): + error = ckan.tests.call_action_api(self.app, action, status=409, + id='joeadmin') + assert 'id' in error + # It should no longer be possible to get warandpeace's follower count. error = ckan.tests.call_action_api(self.app, 'dataset_follower_count', status=409, id='warandpeace') diff --git a/ckan/tests/lib/test_dictization.py b/ckan/tests/lib/test_dictization.py index 61828f0a27d..f868948a59b 100644 --- a/ckan/tests/lib/test_dictization.py +++ b/ckan/tests/lib/test_dictization.py @@ -124,7 +124,10 @@ def setup_class(cls): 'title': u'A Novel By Tolstoy', 'tracking_summary': {'total': 0, 'recent': 0}, 'url': u'http://www.annakarenina.com', - 'version': u'0.7a'} + 'version': u'0.7a', + 'num_tags': 3, + 'num_resources': 2, + } @classmethod @@ -256,6 +259,8 @@ def test_03_package_to_api1(self): asdict = pkg.as_dict() asdict['download_url'] = asdict['resources'][0]['url'] asdict['license_title'] = u'Other (Open)' + asdict['num_tags'] = 3 + asdict['num_resources'] = 2 dictize = package_to_api1(pkg, context) # the is_dict method doesn't care about organizations @@ -274,6 +279,8 @@ def test_04_package_to_api1_with_relationship(self): as_dict = pkg.as_dict() as_dict['license_title'] = None + as_dict['num_tags'] = 0 + as_dict['num_resources'] = 0 dictize = package_to_api1(pkg, context) as_dict["relationships"].sort(key=lambda x:x.items()) @@ -314,6 +321,8 @@ def test_06_package_to_api2_with_relationship(self): as_dict = pkg.as_dict(ref_package_by='id', ref_group_by='id') as_dict['license_title'] = None + as_dict['num_tags'] = 0 + as_dict['num_resources'] = 0 dictize = package_to_api2(pkg, context) as_dict["relationships"].sort(key=lambda x:x.items()) @@ -739,12 +748,15 @@ def test_13_get_package_in_past(self): u'url': u'http://newurl', u'webstore_last_updated': None, u'webstore_url': None}) + third_dictized['num_resources'] = third_dictized['num_resources'] + 1 third_dictized['tags'].insert(1, {'name': u'newnew_tag', 'display_name': u'newnew_tag', 'state': 'active'}) + third_dictized['num_tags'] = third_dictized['num_tags'] + 1 third_dictized['extras'].insert(0, {'key': 'david', 'value': u'"new_value"', 'state': u'active'}) third_dictized['state'] = 'active' + third_dictized['state'] = 'active' pprint(third_dictized) pprint(forth_dictized) diff --git a/ckan/tests/lib/test_dictization_schema.py b/ckan/tests/lib/test_dictization_schema.py index fd99e44f115..1c7f578783f 100644 --- a/ckan/tests/lib/test_dictization_schema.py +++ b/ckan/tests/lib/test_dictization_schema.py @@ -88,12 +88,14 @@ 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'}, diff --git a/ckanext/datastore/plugin.py b/ckanext/datastore/plugin.py index 412ba37f48c..cdb547a2211 100644 --- a/ckanext/datastore/plugin.py +++ b/ckanext/datastore/plugin.py @@ -68,8 +68,7 @@ def configure(self, config): self._create_alias_table() else: log.warn("We detected that CKAN is running on a read only database. " - "Permission checks and _table_metadata creation are skipped." - "Make sure that replication is properly set-up.") + "Permission checks and the creation of _table_metadata are skipped.") else: log.warn("We detected that you do not use a PostgreSQL database. " "The DataStore will NOT work and datastore tests will be skipped.") diff --git a/ckanext/reclinepreview/theme/public/preview_recline.js b/ckanext/reclinepreview/theme/public/preview_recline.js index bf56d89b66c..574d717db99 100644 --- a/ckanext/reclinepreview/theme/public/preview_recline.js +++ b/ckanext/reclinepreview/theme/public/preview_recline.js @@ -7,12 +7,15 @@ this.ckan.module('reclinepreview', function (jQuery, _) { errorDataProxy: "DataProxy returned an error", errorDataStore: "DataStore returned an error", previewNotAvailableForDataType: "Preview not available for data type: " - } + }, + site_url: "" }, initialize: function () { jQuery.proxyAll(this, /_on/); this.el.ready(this._onReady); + // hack to make leaflet use a particular location to look for images + L.Icon.Default.imagePath = this.options.site_url + 'vendor/leaflet/images' }, _onReady: function() { @@ -33,10 +36,7 @@ this.ckan.module('reclinepreview', function (jQuery, _) { function showError(msg){ msg = msg || _('error loading preview'); - return self.el - .append('
    ') - .addClass('alert alert-error fade in') - .html(msg); + window.parent.ckan.pubsub.publish('data-viewer-error', msg); } recline.Backend.DataProxy.timeout = 10000; @@ -84,14 +84,12 @@ this.ckan.module('reclinepreview', function (jQuery, _) { errorMsg = this.options.i18n.errorLoadingPreview + ': ' +this.options.i18n.errorDataProxy; dataset.fetch() .done(function(dataset){ - dataset.bind('query:fail', function (error) { jQuery('.data-view-container', self.el).hide(); jQuery('.header', self.el).hide(); }); self.initializeDataExplorer(dataset); - jQuery('.recline-query-editor .text-query').hide(); }) .fail(function(error){ if (error.message) errorMsg += ' (' + error.message + ')'; @@ -125,18 +123,26 @@ this.ckan.module('reclinepreview', function (jQuery, _) { } ]; + var sidebarViews = [ + { + id: 'filterEditor', + label: 'Filters', + view: new recline.View.FilterEditor({ + model: dataset + }) + } + ]; + var dataExplorer = new recline.View.MultiView({ el: this.el, model: dataset, views: views, + sidebarViews: sidebarViews, config: { readOnly: true } }); - // Hide the fields control by default - // (This should be done in recline!) - // jQuery('.menu-right a[data-action="fields"]', self.el).click(); }, normalizeFormat: function (format) { var out = format.toLowerCase(); @@ -152,4 +158,4 @@ this.ckan.module('reclinepreview', function (jQuery, _) { } } }; -}); \ No newline at end of file +}); diff --git a/ckanext/reclinepreview/theme/public/resource.config b/ckanext/reclinepreview/theme/public/resource.config index 1e946ab6d31..70bc16a7f95 100644 --- a/ckanext/reclinepreview/theme/public/resource.config +++ b/ckanext/reclinepreview/theme/public/resource.config @@ -1,8 +1,11 @@ [IE conditional] +lte IE 7 = + vendor/json/json2.js + lte IE 8 = - vendor/flotr2/excanvas.js - vendor/leaflet/leaflet.ie.css + vendor/flot/excanvas.js + vendor/leaflet/0.4.4/leaflet.ie.css vendor/leaflet.markercluster/MarkerCluster.Default.ie.css [depends] @@ -12,29 +15,31 @@ main = base/main [groups] main = - vendor/underscore/underscore.js - vendor/backbone/backbone.js - vendor/mustache/mustache.js - vendor/bootstrap/bootstrap.js - vendor/flotr2/flotr2.js - vendor/leaflet/leaflet.js + vendor/underscore/1.4.2/underscore.js + vendor/backbone/0.9.2/backbone.js + vendor/mustache/0.5.0-dev/mustache.js + vendor/bootstrap/2.0.2/bootstrap.js + vendor/json/json2.js + vendor/flot/excanvas.js + vendor/flot/jquery.flot.js + vendor/leaflet/0.4.4/leaflet.js vendor/leaflet.markercluster/leaflet.markercluster.js - vendor/slickgrid/jquery-ui-1.8.16.custom.js - vendor/slickgrid/jquery.event.drag-2.0.js - vendor/slickgrid/slick.grid.js - vendor/moment/moment.js + vendor/slickgrid/2.0.1/jquery-ui-1.8.16.custom.js + vendor/slickgrid/2.0.1/jquery.event.drag-2.0.js + vendor/slickgrid/2.0.1/slick.grid.js + vendor/moment/1.6.2/moment.js vendor/recline/recline.js preview_recline.js - vendor/bootstrap/css/bootstrap.css - vendor/leaflet/leaflet.css - vendor/leaflet/leaflet.ie.css + vendor/bootstrap/2.0.2/css/bootstrap.css + vendor/leaflet/0.4.4/leaflet.css + vendor/leaflet/0.4.4/leaflet.ie.css vendor/leaflet.markercluster/MarkerCluster.css vendor/leaflet.markercluster/MarkerCluster.Default.css vendor/leaflet.markercluster/MarkerCluster.Default.ie.css - vendor/slickgrid/slick.grid.css - vendor/recline/css/recline.css + vendor/slickgrid/2.0.1/slick.grid.css + vendor/recline/recline.css css/recline.css diff --git a/ckanext/reclinepreview/theme/public/vendor/backbone/0.9.2/backbone.js b/ckanext/reclinepreview/theme/public/vendor/backbone/0.9.2/backbone.js new file mode 100644 index 00000000000..3373c952bfa --- /dev/null +++ b/ckanext/reclinepreview/theme/public/vendor/backbone/0.9.2/backbone.js @@ -0,0 +1,1431 @@ +// Backbone.js 0.9.2 + +// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://backbonejs.org + +(function(){ + + // Initial Setup + // ------------- + + // Save a reference to the global object (`window` in the browser, `global` + // on the server). + var root = this; + + // Save the previous value of the `Backbone` variable, so that it can be + // restored later on, if `noConflict` is used. + var previousBackbone = root.Backbone; + + // Create a local reference to slice/splice. + var slice = Array.prototype.slice; + var splice = Array.prototype.splice; + + // The top-level namespace. All public Backbone classes and modules will + // be attached to this. Exported for both CommonJS and the browser. + var Backbone; + if (typeof exports !== 'undefined') { + Backbone = exports; + } else { + Backbone = root.Backbone = {}; + } + + // Current version of the library. Keep in sync with `package.json`. + Backbone.VERSION = '0.9.2'; + + // Require Underscore, if we're on the server, and it's not already present. + var _ = root._; + if (!_ && (typeof require !== 'undefined')) _ = require('underscore'); + + // For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable. + var $ = root.jQuery || root.Zepto || root.ender; + + // Set the JavaScript library that will be used for DOM manipulation and + // Ajax calls (a.k.a. the `$` variable). By default Backbone will use: jQuery, + // Zepto, or Ender; but the `setDomLibrary()` method lets you inject an + // alternate JavaScript library (or a mock library for testing your views + // outside of a browser). + Backbone.setDomLibrary = function(lib) { + $ = lib; + }; + + // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable + // to its previous owner. Returns a reference to this Backbone object. + Backbone.noConflict = function() { + root.Backbone = previousBackbone; + return this; + }; + + // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option + // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and + // set a `X-Http-Method-Override` header. + Backbone.emulateHTTP = false; + + // Turn on `emulateJSON` to support legacy servers that can't deal with direct + // `application/json` requests ... will encode the body as + // `application/x-www-form-urlencoded` instead and will send the model in a + // form param named `model`. + Backbone.emulateJSON = false; + + // Backbone.Events + // ----------------- + + // Regular expression used to split event strings + var eventSplitter = /\s+/; + + // A module that can be mixed in to *any object* in order to provide it with + // custom events. You may bind with `on` or remove with `off` callback functions + // to an event; trigger`-ing an event fires all callbacks in succession. + // + // var object = {}; + // _.extend(object, Backbone.Events); + // object.on('expand', function(){ alert('expanded'); }); + // object.trigger('expand'); + // + var Events = Backbone.Events = { + + // Bind one or more space separated events, `events`, to a `callback` + // function. Passing `"all"` will bind the callback to all events fired. + on: function(events, callback, context) { + + var calls, event, node, tail, list; + if (!callback) return this; + events = events.split(eventSplitter); + calls = this._callbacks || (this._callbacks = {}); + + // Create an immutable callback list, allowing traversal during + // modification. The tail is an empty object that will always be used + // as the next node. + while (event = events.shift()) { + list = calls[event]; + node = list ? list.tail : {}; + node.next = tail = {}; + node.context = context; + node.callback = callback; + calls[event] = {tail: tail, next: list ? list.next : node}; + } + + return this; + }, + + // Remove one or many callbacks. If `context` is null, removes all callbacks + // with that function. If `callback` is null, removes all callbacks for the + // event. If `events` is null, removes all bound callbacks for all events. + off: function(events, callback, context) { + var event, calls, node, tail, cb, ctx; + + // No events, or removing *all* events. + if (!(calls = this._callbacks)) return; + if (!(events || callback || context)) { + delete this._callbacks; + return this; + } + + // Loop through the listed events and contexts, splicing them out of the + // linked list of callbacks if appropriate. + events = events ? events.split(eventSplitter) : _.keys(calls); + while (event = events.shift()) { + node = calls[event]; + delete calls[event]; + if (!node || !(callback || context)) continue; + // Create a new list, omitting the indicated callbacks. + tail = node.tail; + while ((node = node.next) !== tail) { + cb = node.callback; + ctx = node.context; + if ((callback && cb !== callback) || (context && ctx !== context)) { + this.on(event, cb, ctx); + } + } + } + + return this; + }, + + // Trigger one or many events, firing all bound callbacks. Callbacks are + // passed the same arguments as `trigger` is, apart from the event name + // (unless you're listening on `"all"`, which will cause your callback to + // receive the true name of the event as the first argument). + trigger: function(events) { + var event, node, calls, tail, args, all, rest; + if (!(calls = this._callbacks)) return this; + all = calls.all; + events = events.split(eventSplitter); + rest = slice.call(arguments, 1); + + // For each event, walk through the linked list of callbacks twice, + // first to trigger the event, then to trigger any `"all"` callbacks. + while (event = events.shift()) { + if (node = calls[event]) { + tail = node.tail; + while ((node = node.next) !== tail) { + node.callback.apply(node.context || this, rest); + } + } + if (node = all) { + tail = node.tail; + args = [event].concat(rest); + while ((node = node.next) !== tail) { + node.callback.apply(node.context || this, args); + } + } + } + + return this; + } + + }; + + // Aliases for backwards compatibility. + Events.bind = Events.on; + Events.unbind = Events.off; + + // Backbone.Model + // -------------- + + // Create a new model, with defined attributes. A client id (`cid`) + // is automatically generated and assigned for you. + var Model = Backbone.Model = function(attributes, options) { + var defaults; + attributes || (attributes = {}); + if (options && options.parse) attributes = this.parse(attributes); + if (defaults = getValue(this, 'defaults')) { + attributes = _.extend({}, defaults, attributes); + } + if (options && options.collection) this.collection = options.collection; + this.attributes = {}; + this._escapedAttributes = {}; + this.cid = _.uniqueId('c'); + this.changed = {}; + this._silent = {}; + this._pending = {}; + this.set(attributes, {silent: true}); + // Reset change tracking. + this.changed = {}; + this._silent = {}; + this._pending = {}; + this._previousAttributes = _.clone(this.attributes); + this.initialize.apply(this, arguments); + }; + + // Attach all inheritable methods to the Model prototype. + _.extend(Model.prototype, Events, { + + // A hash of attributes whose current and previous value differ. + changed: null, + + // A hash of attributes that have silently changed since the last time + // `change` was called. Will become pending attributes on the next call. + _silent: null, + + // A hash of attributes that have changed since the last `'change'` event + // began. + _pending: null, + + // The default name for the JSON `id` attribute is `"id"`. MongoDB and + // CouchDB users may want to set this to `"_id"`. + idAttribute: 'id', + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Return a copy of the model's `attributes` object. + toJSON: function(options) { + return _.clone(this.attributes); + }, + + // Get the value of an attribute. + get: function(attr) { + return this.attributes[attr]; + }, + + // Get the HTML-escaped value of an attribute. + escape: function(attr) { + var html; + if (html = this._escapedAttributes[attr]) return html; + var val = this.get(attr); + return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val); + }, + + // Returns `true` if the attribute contains a value that is not null + // or undefined. + has: function(attr) { + return this.get(attr) != null; + }, + + // Set a hash of model attributes on the object, firing `"change"` unless + // you choose to silence it. + set: function(key, value, options) { + var attrs, attr, val; + + // Handle both `"key", value` and `{key: value}` -style arguments. + if (_.isObject(key) || key == null) { + attrs = key; + options = value; + } else { + attrs = {}; + attrs[key] = value; + } + + // Extract attributes and options. + options || (options = {}); + if (!attrs) return this; + if (attrs instanceof Model) attrs = attrs.attributes; + if (options.unset) for (attr in attrs) attrs[attr] = void 0; + + // Run validation. + if (!this._validate(attrs, options)) return false; + + // Check for changes of `id`. + if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; + + var changes = options.changes = {}; + var now = this.attributes; + var escaped = this._escapedAttributes; + var prev = this._previousAttributes || {}; + + // For each `set` attribute... + for (attr in attrs) { + val = attrs[attr]; + + // If the new and current value differ, record the change. + if (!_.isEqual(now[attr], val) || (options.unset && _.has(now, attr))) { + delete escaped[attr]; + (options.silent ? this._silent : changes)[attr] = true; + } + + // Update or delete the current value. + options.unset ? delete now[attr] : now[attr] = val; + + // If the new and previous value differ, record the change. If not, + // then remove changes for this attribute. + if (!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) { + this.changed[attr] = val; + if (!options.silent) this._pending[attr] = true; + } else { + delete this.changed[attr]; + delete this._pending[attr]; + } + } + + // Fire the `"change"` events. + if (!options.silent) this.change(options); + return this; + }, + + // Remove an attribute from the model, firing `"change"` unless you choose + // to silence it. `unset` is a noop if the attribute doesn't exist. + unset: function(attr, options) { + (options || (options = {})).unset = true; + return this.set(attr, null, options); + }, + + // Clear all attributes on the model, firing `"change"` unless you choose + // to silence it. + clear: function(options) { + (options || (options = {})).unset = true; + return this.set(_.clone(this.attributes), options); + }, + + // Fetch the model from the server. If the server's representation of the + // model differs from its current attributes, they will be overriden, + // triggering a `"change"` event. + fetch: function(options) { + options = options ? _.clone(options) : {}; + var model = this; + var success = options.success; + options.success = function(resp, status, xhr) { + if (!model.set(model.parse(resp, xhr), options)) return false; + if (success) success(model, resp); + }; + options.error = Backbone.wrapError(options.error, model, options); + return (this.sync || Backbone.sync).call(this, 'read', this, options); + }, + + // Set a hash of model attributes, and sync the model to the server. + // If the server returns an attributes hash that differs, the model's + // state will be `set` again. + save: function(key, value, options) { + var attrs, current; + + // Handle both `("key", value)` and `({key: value})` -style calls. + if (_.isObject(key) || key == null) { + attrs = key; + options = value; + } else { + attrs = {}; + attrs[key] = value; + } + options = options ? _.clone(options) : {}; + + // If we're "wait"-ing to set changed attributes, validate early. + if (options.wait) { + if (!this._validate(attrs, options)) return false; + current = _.clone(this.attributes); + } + + // Regular saves `set` attributes before persisting to the server. + var silentOptions = _.extend({}, options, {silent: true}); + if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) { + return false; + } + + // After a successful server-side save, the client is (optionally) + // updated with the server-side state. + var model = this; + var success = options.success; + options.success = function(resp, status, xhr) { + var serverAttrs = model.parse(resp, xhr); + if (options.wait) { + delete options.wait; + serverAttrs = _.extend(attrs || {}, serverAttrs); + } + if (!model.set(serverAttrs, options)) return false; + if (success) { + success(model, resp); + } else { + model.trigger('sync', model, resp, options); + } + }; + + // Finish configuring and sending the Ajax request. + options.error = Backbone.wrapError(options.error, model, options); + var method = this.isNew() ? 'create' : 'update'; + var xhr = (this.sync || Backbone.sync).call(this, method, this, options); + if (options.wait) this.set(current, silentOptions); + return xhr; + }, + + // Destroy this model on the server if it was already persisted. + // Optimistically removes the model from its collection, if it has one. + // If `wait: true` is passed, waits for the server to respond before removal. + destroy: function(options) { + options = options ? _.clone(options) : {}; + var model = this; + var success = options.success; + + var triggerDestroy = function() { + model.trigger('destroy', model, model.collection, options); + }; + + if (this.isNew()) { + triggerDestroy(); + return false; + } + + options.success = function(resp) { + if (options.wait) triggerDestroy(); + if (success) { + success(model, resp); + } else { + model.trigger('sync', model, resp, options); + } + }; + + options.error = Backbone.wrapError(options.error, model, options); + var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options); + if (!options.wait) triggerDestroy(); + return xhr; + }, + + // Default URL for the model's representation on the server -- if you're + // using Backbone's restful methods, override this to change the endpoint + // that will be called. + url: function() { + var base = getValue(this, 'urlRoot') || getValue(this.collection, 'url') || urlError(); + if (this.isNew()) return base; + return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id); + }, + + // **parse** converts a response into the hash of attributes to be `set` on + // the model. The default implementation is just to pass the response along. + parse: function(resp, xhr) { + return resp; + }, + + // Create a new model with identical attributes to this one. + clone: function() { + return new this.constructor(this.attributes); + }, + + // A model is new if it has never been saved to the server, and lacks an id. + isNew: function() { + return this.id == null; + }, + + // Call this method to manually fire a `"change"` event for this model and + // a `"change:attribute"` event for each changed attribute. + // Calling this will cause all objects observing the model to update. + change: function(options) { + options || (options = {}); + var changing = this._changing; + this._changing = true; + + // Silent changes become pending changes. + for (var attr in this._silent) this._pending[attr] = true; + + // Silent changes are triggered. + var changes = _.extend({}, options.changes, this._silent); + this._silent = {}; + for (var attr in changes) { + this.trigger('change:' + attr, this, this.get(attr), options); + } + if (changing) return this; + + // Continue firing `"change"` events while there are pending changes. + while (!_.isEmpty(this._pending)) { + this._pending = {}; + this.trigger('change', this, options); + // Pending and silent changes still remain. + for (var attr in this.changed) { + if (this._pending[attr] || this._silent[attr]) continue; + delete this.changed[attr]; + } + this._previousAttributes = _.clone(this.attributes); + } + + this._changing = false; + return this; + }, + + // Determine if the model has changed since the last `"change"` event. + // If you specify an attribute name, determine if that attribute has changed. + hasChanged: function(attr) { + if (!arguments.length) return !_.isEmpty(this.changed); + return _.has(this.changed, attr); + }, + + // Return an object containing all the attributes that have changed, or + // false if there are no changed attributes. Useful for determining what + // parts of a view need to be updated and/or what attributes need to be + // persisted to the server. Unset attributes will be set to undefined. + // You can also pass an attributes object to diff against the model, + // determining if there *would be* a change. + changedAttributes: function(diff) { + if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; + var val, changed = false, old = this._previousAttributes; + for (var attr in diff) { + if (_.isEqual(old[attr], (val = diff[attr]))) continue; + (changed || (changed = {}))[attr] = val; + } + return changed; + }, + + // Get the previous value of an attribute, recorded at the time the last + // `"change"` event was fired. + previous: function(attr) { + if (!arguments.length || !this._previousAttributes) return null; + return this._previousAttributes[attr]; + }, + + // Get all of the attributes of the model at the time of the previous + // `"change"` event. + previousAttributes: function() { + return _.clone(this._previousAttributes); + }, + + // Check if the model is currently in a valid state. It's only possible to + // get into an *invalid* state if you're using silent changes. + isValid: function() { + return !this.validate(this.attributes); + }, + + // Run validation against the next complete set of model attributes, + // returning `true` if all is well. If a specific `error` callback has + // been passed, call that instead of firing the general `"error"` event. + _validate: function(attrs, options) { + if (options.silent || !this.validate) return true; + attrs = _.extend({}, this.attributes, attrs); + var error = this.validate(attrs, options); + if (!error) return true; + if (options && options.error) { + options.error(this, error, options); + } else { + this.trigger('error', this, error, options); + } + return false; + } + + }); + + // Backbone.Collection + // ------------------- + + // Provides a standard collection class for our sets of models, ordered + // or unordered. If a `comparator` is specified, the Collection will maintain + // its models in sort order, as they're added and removed. + var Collection = Backbone.Collection = function(models, options) { + options || (options = {}); + if (options.model) this.model = options.model; + if (options.comparator) this.comparator = options.comparator; + this._reset(); + this.initialize.apply(this, arguments); + if (models) this.reset(models, {silent: true, parse: options.parse}); + }; + + // Define the Collection's inheritable methods. + _.extend(Collection.prototype, Events, { + + // The default model for a collection is just a **Backbone.Model**. + // This should be overridden in most cases. + model: Model, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // The JSON representation of a Collection is an array of the + // models' attributes. + toJSON: function(options) { + return this.map(function(model){ return model.toJSON(options); }); + }, + + // Add a model, or list of models to the set. Pass **silent** to avoid + // firing the `add` event for every new model. + add: function(models, options) { + var i, index, length, model, cid, id, cids = {}, ids = {}, dups = []; + options || (options = {}); + models = _.isArray(models) ? models.slice() : [models]; + + // Begin by turning bare objects into model references, and preventing + // invalid models or duplicate models from being added. + for (i = 0, length = models.length; i < length; i++) { + if (!(model = models[i] = this._prepareModel(models[i], options))) { + throw new Error("Can't add an invalid model to a collection"); + } + cid = model.cid; + id = model.id; + if (cids[cid] || this._byCid[cid] || ((id != null) && (ids[id] || this._byId[id]))) { + dups.push(i); + continue; + } + cids[cid] = ids[id] = model; + } + + // Remove duplicates. + i = dups.length; + while (i--) { + models.splice(dups[i], 1); + } + + // Listen to added models' events, and index models for lookup by + // `id` and by `cid`. + for (i = 0, length = models.length; i < length; i++) { + (model = models[i]).on('all', this._onModelEvent, this); + this._byCid[model.cid] = model; + if (model.id != null) this._byId[model.id] = model; + } + + // Insert models into the collection, re-sorting if needed, and triggering + // `add` events unless silenced. + this.length += length; + index = options.at != null ? options.at : this.models.length; + splice.apply(this.models, [index, 0].concat(models)); + if (this.comparator) this.sort({silent: true}); + if (options.silent) return this; + for (i = 0, length = this.models.length; i < length; i++) { + if (!cids[(model = this.models[i]).cid]) continue; + options.index = i; + model.trigger('add', model, this, options); + } + return this; + }, + + // Remove a model, or a list of models from the set. Pass silent to avoid + // firing the `remove` event for every model removed. + remove: function(models, options) { + var i, l, index, model; + options || (options = {}); + models = _.isArray(models) ? models.slice() : [models]; + for (i = 0, l = models.length; i < l; i++) { + model = this.getByCid(models[i]) || this.get(models[i]); + if (!model) continue; + delete this._byId[model.id]; + delete this._byCid[model.cid]; + index = this.indexOf(model); + this.models.splice(index, 1); + this.length--; + if (!options.silent) { + options.index = index; + model.trigger('remove', model, this, options); + } + this._removeReference(model); + } + return this; + }, + + // Add a model to the end of the collection. + push: function(model, options) { + model = this._prepareModel(model, options); + this.add(model, options); + return model; + }, + + // Remove a model from the end of the collection. + pop: function(options) { + var model = this.at(this.length - 1); + this.remove(model, options); + return model; + }, + + // Add a model to the beginning of the collection. + unshift: function(model, options) { + model = this._prepareModel(model, options); + this.add(model, _.extend({at: 0}, options)); + return model; + }, + + // Remove a model from the beginning of the collection. + shift: function(options) { + var model = this.at(0); + this.remove(model, options); + return model; + }, + + // Get a model from the set by id. + get: function(id) { + if (id == null) return void 0; + return this._byId[id.id != null ? id.id : id]; + }, + + // Get a model from the set by client id. + getByCid: function(cid) { + return cid && this._byCid[cid.cid || cid]; + }, + + // Get the model at the given index. + at: function(index) { + return this.models[index]; + }, + + // Return models with matching attributes. Useful for simple cases of `filter`. + where: function(attrs) { + if (_.isEmpty(attrs)) return []; + return this.filter(function(model) { + for (var key in attrs) { + if (attrs[key] !== model.get(key)) return false; + } + return true; + }); + }, + + // Force the collection to re-sort itself. You don't need to call this under + // normal circumstances, as the set will maintain sort order as each item + // is added. + sort: function(options) { + options || (options = {}); + if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); + var boundComparator = _.bind(this.comparator, this); + if (this.comparator.length == 1) { + this.models = this.sortBy(boundComparator); + } else { + this.models.sort(boundComparator); + } + if (!options.silent) this.trigger('reset', this, options); + return this; + }, + + // Pluck an attribute from each model in the collection. + pluck: function(attr) { + return _.map(this.models, function(model){ return model.get(attr); }); + }, + + // When you have more items than you want to add or remove individually, + // you can reset the entire set with a new list of models, without firing + // any `add` or `remove` events. Fires `reset` when finished. + reset: function(models, options) { + models || (models = []); + options || (options = {}); + for (var i = 0, l = this.models.length; i < l; i++) { + this._removeReference(this.models[i]); + } + this._reset(); + this.add(models, _.extend({silent: true}, options)); + if (!options.silent) this.trigger('reset', this, options); + return this; + }, + + // Fetch the default set of models for this collection, resetting the + // collection when they arrive. If `add: true` is passed, appends the + // models to the collection instead of resetting. + fetch: function(options) { + options = options ? _.clone(options) : {}; + if (options.parse === undefined) options.parse = true; + var collection = this; + var success = options.success; + options.success = function(resp, status, xhr) { + collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options); + if (success) success(collection, resp); + }; + options.error = Backbone.wrapError(options.error, collection, options); + return (this.sync || Backbone.sync).call(this, 'read', this, options); + }, + + // Create a new instance of a model in this collection. Add the model to the + // collection immediately, unless `wait: true` is passed, in which case we + // wait for the server to agree. + create: function(model, options) { + var coll = this; + options = options ? _.clone(options) : {}; + model = this._prepareModel(model, options); + if (!model) return false; + if (!options.wait) coll.add(model, options); + var success = options.success; + options.success = function(nextModel, resp, xhr) { + if (options.wait) coll.add(nextModel, options); + if (success) { + success(nextModel, resp); + } else { + nextModel.trigger('sync', model, resp, options); + } + }; + model.save(null, options); + return model; + }, + + // **parse** converts a response into a list of models to be added to the + // collection. The default implementation is just to pass it through. + parse: function(resp, xhr) { + return resp; + }, + + // Proxy to _'s chain. Can't be proxied the same way the rest of the + // underscore methods are proxied because it relies on the underscore + // constructor. + chain: function () { + return _(this.models).chain(); + }, + + // Reset all internal state. Called when the collection is reset. + _reset: function(options) { + this.length = 0; + this.models = []; + this._byId = {}; + this._byCid = {}; + }, + + // Prepare a model or hash of attributes to be added to this collection. + _prepareModel: function(model, options) { + options || (options = {}); + if (!(model instanceof Model)) { + var attrs = model; + options.collection = this; + model = new this.model(attrs, options); + if (!model._validate(model.attributes, options)) model = false; + } else if (!model.collection) { + model.collection = this; + } + return model; + }, + + // Internal method to remove a model's ties to a collection. + _removeReference: function(model) { + if (this == model.collection) { + delete model.collection; + } + model.off('all', this._onModelEvent, this); + }, + + // Internal method called every time a model in the set fires an event. + // Sets need to update their indexes when models change ids. All other + // events simply proxy through. "add" and "remove" events that originate + // in other collections are ignored. + _onModelEvent: function(event, model, collection, options) { + if ((event == 'add' || event == 'remove') && collection != this) return; + if (event == 'destroy') { + this.remove(model, options); + } + if (model && event === 'change:' + model.idAttribute) { + delete this._byId[model.previous(model.idAttribute)]; + this._byId[model.id] = model; + } + this.trigger.apply(this, arguments); + } + + }); + + // Underscore methods that we want to implement on the Collection. + var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', + 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', + 'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', + 'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf', + 'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy']; + + // Mix in each Underscore method as a proxy to `Collection#models`. + _.each(methods, function(method) { + Collection.prototype[method] = function() { + return _[method].apply(_, [this.models].concat(_.toArray(arguments))); + }; + }); + + // Backbone.Router + // ------------------- + + // Routers map faux-URLs to actions, and fire events when routes are + // matched. Creating a new one sets its `routes` hash, if not set statically. + var Router = Backbone.Router = function(options) { + options || (options = {}); + if (options.routes) this.routes = options.routes; + this._bindRoutes(); + this.initialize.apply(this, arguments); + }; + + // Cached regular expressions for matching named param parts and splatted + // parts of route strings. + var namedParam = /:\w+/g; + var splatParam = /\*\w+/g; + var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g; + + // Set up all inheritable **Backbone.Router** properties and methods. + _.extend(Router.prototype, Events, { + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Manually bind a single named route to a callback. For example: + // + // this.route('search/:query/p:num', 'search', function(query, num) { + // ... + // }); + // + route: function(route, name, callback) { + Backbone.history || (Backbone.history = new History); + if (!_.isRegExp(route)) route = this._routeToRegExp(route); + if (!callback) callback = this[name]; + Backbone.history.route(route, _.bind(function(fragment) { + var args = this._extractParameters(route, fragment); + callback && callback.apply(this, args); + this.trigger.apply(this, ['route:' + name].concat(args)); + Backbone.history.trigger('route', this, name, args); + }, this)); + return this; + }, + + // Simple proxy to `Backbone.history` to save a fragment into the history. + navigate: function(fragment, options) { + Backbone.history.navigate(fragment, options); + }, + + // Bind all defined routes to `Backbone.history`. We have to reverse the + // order of the routes here to support behavior where the most general + // routes can be defined at the bottom of the route map. + _bindRoutes: function() { + if (!this.routes) return; + var routes = []; + for (var route in this.routes) { + routes.unshift([route, this.routes[route]]); + } + for (var i = 0, l = routes.length; i < l; i++) { + this.route(routes[i][0], routes[i][1], this[routes[i][1]]); + } + }, + + // Convert a route string into a regular expression, suitable for matching + // against the current location hash. + _routeToRegExp: function(route) { + route = route.replace(escapeRegExp, '\\$&') + .replace(namedParam, '([^\/]+)') + .replace(splatParam, '(.*?)'); + return new RegExp('^' + route + '$'); + }, + + // Given a route, and a URL fragment that it matches, return the array of + // extracted parameters. + _extractParameters: function(route, fragment) { + return route.exec(fragment).slice(1); + } + + }); + + // Backbone.History + // ---------------- + + // Handles cross-browser history management, based on URL fragments. If the + // browser does not support `onhashchange`, falls back to polling. + var History = Backbone.History = function() { + this.handlers = []; + _.bindAll(this, 'checkUrl'); + }; + + // Cached regex for cleaning leading hashes and slashes . + var routeStripper = /^[#\/]/; + + // Cached regex for detecting MSIE. + var isExplorer = /msie [\w.]+/; + + // Has the history handling already been started? + History.started = false; + + // Set up all inheritable **Backbone.History** properties and methods. + _.extend(History.prototype, Events, { + + // The default interval to poll for hash changes, if necessary, is + // twenty times a second. + interval: 50, + + // Gets the true hash value. Cannot use location.hash directly due to bug + // in Firefox where location.hash will always be decoded. + getHash: function(windowOverride) { + var loc = windowOverride ? windowOverride.location : window.location; + var match = loc.href.match(/#(.*)$/); + return match ? match[1] : ''; + }, + + // Get the cross-browser normalized URL fragment, either from the URL, + // the hash, or the override. + getFragment: function(fragment, forcePushState) { + if (fragment == null) { + if (this._hasPushState || forcePushState) { + fragment = window.location.pathname; + var search = window.location.search; + if (search) fragment += search; + } else { + fragment = this.getHash(); + } + } + if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length); + return fragment.replace(routeStripper, ''); + }, + + // Start the hash change handling, returning `true` if the current URL matches + // an existing route, and `false` otherwise. + start: function(options) { + if (History.started) throw new Error("Backbone.history has already been started"); + History.started = true; + + // Figure out the initial configuration. Do we need an iframe? + // Is pushState desired ... is it available? + this.options = _.extend({}, {root: '/'}, this.options, options); + this._wantsHashChange = this.options.hashChange !== false; + this._wantsPushState = !!this.options.pushState; + this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState); + var fragment = this.getFragment(); + var docMode = document.documentMode; + var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); + + if (oldIE) { + this.iframe = $('':"");a._keyEvent=false;return w},_generateMonthYearHeader:function(a,b,c,e,f,h,i,g){var j=this._get(a,"changeMonth"),l=this._get(a,"changeYear"),u=this._get(a,"showMonthAfterYear"),k='
    ',o="";if(h||!j)o+=''+i[b]+"";else{i=e&&e.getFullYear()==c;var m=f&&f.getFullYear()==c;o+='"}u||(k+=o+(h||!(j&&l)?" ":""));if(!a.yearshtml){a.yearshtml="";if(h||!l)k+=''+c+"";else{g=this._get(a,"yearRange").split(":");var s=(new Date).getFullYear();i=function(q){q=q.match(/c[+-].*/)?c+parseInt(q.substring(1),10):q.match(/[+-].*/)?s+parseInt(q,10):parseInt(q,10);return isNaN(q)?s:q};b=i(g[0]);g=Math.max(b,i(g[1]||""));b=e?Math.max(b,e.getFullYear()):b;g=f?Math.min(g,f.getFullYear()):g;for(a.yearshtml+='";k+=a.yearshtml;a.yearshtml=null}}k+=this._get(a,"yearSuffix");if(u)k+=(h||!(j&&l)?" ":"")+o;k+="
    ";return k},_adjustInstDate:function(a,b,c){var e=a.drawYear+(c=="Y"?b:0),f=a.drawMonth+ -(c=="M"?b:0);b=Math.min(a.selectedDay,this._getDaysInMonth(e,f))+(c=="D"?b:0);e=this._restrictMinMax(a,this._daylightSavingAdjust(new Date(e,f,b)));a.selectedDay=e.getDate();a.drawMonth=a.selectedMonth=e.getMonth();a.drawYear=a.selectedYear=e.getFullYear();if(c=="M"||c=="Y")this._notifyChange(a)},_restrictMinMax:function(a,b){var c=this._getMinMaxDate(a,"min");a=this._getMinMaxDate(a,"max");b=c&&ba?a:b},_notifyChange:function(a){var b=this._get(a,"onChangeMonthYear");if(b)b.apply(a.input?a.input[0]:null,[a.selectedYear,a.selectedMonth+1,a])},_getNumberOfMonths:function(a){a=this._get(a,"numberOfMonths");return a==null?[1,1]:typeof a=="number"?[1,a]:a},_getMinMaxDate:function(a,b){return this._determineDate(a,this._get(a,b+"Date"),null)},_getDaysInMonth:function(a,b){return 32-this._daylightSavingAdjust(new Date(a,b,32)).getDate()},_getFirstDayOfMonth:function(a,b){return(new Date(a,b,1)).getDay()},_canAdjustMonth:function(a,b,c,e){var f=this._getNumberOfMonths(a);c=this._daylightSavingAdjust(new Date(c,e+(b<0?b:f[0]*f[1]),1));b<0&&c.setDate(this._getDaysInMonth(c.getFullYear(),c.getMonth()));return this._isInRange(a,c)},_isInRange:function(a,b){var c=this._getMinMaxDate(a,"min");a=this._getMinMaxDate(a,"max");return(!c||b.getTime()>=c.getTime())&&(!a||b.getTime()<=a.getTime())},_getFormatConfig:function(a){var b=this._get(a,"shortYearCutoff");b=typeof b!="string"?b:(new Date).getFullYear()%100+parseInt(b,10);return{shortYearCutoff:b,dayNamesShort:this._get(a,"dayNamesShort"),dayNames:this._get(a,"dayNames"),monthNamesShort:this._get(a,"monthNamesShort"),monthNames:this._get(a,"monthNames")}},_formatDate:function(a,b,c,e){if(!b){a.currentDay=a.selectedDay;a.currentMonth=a.selectedMonth;a.currentYear=a.selectedYear}b=b?typeof b=="object"?b:this._daylightSavingAdjust(new Date(e,c,b)):this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay));return this.formatDate(this._get(a,"dateFormat"),b,this._getFormatConfig(a))}});d.fn.datepicker=function(a){if(!this.length)return this;if(!d.datepicker.initialized){d(document).mousedown(d.datepicker._checkExternalClick).find("body").append(d.datepicker.dpDiv);d.datepicker.initialized=true}var b=Array.prototype.slice.call(arguments,1);if(typeof a=="string"&&(a=="isDisabled"||a=="getDate"||a=="widget"))return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b));if(a=="option"&&arguments.length==2&&typeof arguments[1]=="string")return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b));return this.each(function(){typeof a=="string"?d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this].concat(b)):d.datepicker._attachDatepicker(this,a)})};d.datepicker=new M;d.datepicker.initialized=false;d.datepicker.uuid=(new Date).getTime();d.datepicker.version="1.8.16";window["DP_jQuery_"+B]=d})(jQuery);;jQuery.effects||function(f,j){function m(c){var a;if(c&&c.constructor==Array&&c.length==3)return c;if(a=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(c))return[parseInt(a[1],10),parseInt(a[2],10),parseInt(a[3],10)];if(a=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(c))return[parseFloat(a[1])*2.55,parseFloat(a[2])*2.55,parseFloat(a[3])*2.55];if(a=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(c))return[parseInt(a[1],16),parseInt(a[2],16),parseInt(a[3],16)];if(a=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(c))return[parseInt(a[1]+a[1],16),parseInt(a[2]+a[2],16),parseInt(a[3]+a[3],16)];if(/rgba\(0, 0, 0, 0\)/.exec(c))return n.transparent;return n[f.trim(c).toLowerCase()]}function s(c,a){var b;do{b=f.curCSS(c,a);if(b!=""&&b!="transparent"||f.nodeName(c,"body"))break;a="backgroundColor"}while(c=c.parentNode);return m(b)}function o(){var c=document.defaultView?document.defaultView.getComputedStyle(this,null):this.currentStyle,a={},b,d;if(c&&c.length&&c[0]&&c[c[0]])for(var e=c.length;e--;){b=c[e];if(typeof c[b]=="string"){d=b.replace(/\-(\w)/g,function(g,h){return h.toUpperCase()});a[d]=c[b]}}else for(b in c)if(typeof c[b]==="string")a[b]=c[b];return a}function p(c){var a,b;for(a in c){b=c[a];if(b==null||f.isFunction(b)||a in t||/scrollbar/.test(a)||!/color/i.test(a)&&isNaN(parseFloat(b)))delete c[a]}return c}function u(c,a){var b={_:0},d;for(d in a)if(c[d]!=a[d])b[d]=a[d];return b}function k(c,a,b,d){if(typeof c=="object"){d=a;b=null;a=c;c=a.effect}if(f.isFunction(a)){d=a;b=null;a={}}if(typeof a=="number"||f.fx.speeds[a]){d=b;b=a;a={}}if(f.isFunction(b)){d=b;b=null}a=a||{};b=b||a.duration;b=f.fx.off?0:typeof b=="number"?b:b in f.fx.speeds?f.fx.speeds[b]:f.fx.speeds._default;d=d||a.complete;return[c,a,b,d]}function l(c){if(!c||typeof c==="number"||f.fx.speeds[c])return true;if(typeof c==="string"&&!f.effects[c])return true;return false}f.effects={};f.each(["backgroundColor","borderBottomColor","borderLeftColor","borderRightColor","borderTopColor","borderColor","color","outlineColor"],function(c,a){f.fx.step[a]=function(b){if(!b.colorInit){b.start=s(b.elem,a);b.end=m(b.end);b.colorInit=true}b.elem.style[a]="rgb("+Math.max(Math.min(parseInt(b.pos*(b.end[0]-b.start[0])+b.start[0],10),255),0)+","+Math.max(Math.min(parseInt(b.pos*(b.end[1]-b.start[1])+b.start[1],10),255),0)+","+Math.max(Math.min(parseInt(b.pos*(b.end[2]-b.start[2])+b.start[2],10),255),0)+")"}});var n={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0],transparent:[255,255,255]},q=["add","remove","toggle"],t={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};f.effects.animateClass=function(c,a,b,d){if(f.isFunction(b)){d=b;b=null}return this.queue(function(){var e=f(this),g=e.attr("style")||" ",h=p(o.call(this)),r,v=e.attr("class");f.each(q,function(w,i){c[i]&&e[i+"Class"](c[i])});r=p(o.call(this));e.attr("class",v);e.animate(u(h,r),{queue:false,duration:a,easing:b,complete:function(){f.each(q,function(w,i){c[i]&&e[i+"Class"](c[i])});if(typeof e.attr("style")=="object"){e.attr("style").cssText="";e.attr("style").cssText=g}else e.attr("style",g);d&&d.apply(this,arguments);f.dequeue(this)}})})};f.fn.extend({_addClass:f.fn.addClass,addClass:function(c,a,b,d){return a?f.effects.animateClass.apply(this,[{add:c},a,b,d]):this._addClass(c)},_removeClass:f.fn.removeClass,removeClass:function(c,a,b,d){return a?f.effects.animateClass.apply(this,[{remove:c},a,b,d]):this._removeClass(c)},_toggleClass:f.fn.toggleClass,toggleClass:function(c,a,b,d,e){return typeof a=="boolean"||a===j?b?f.effects.animateClass.apply(this,[a?{add:c}:{remove:c},b,d,e]):this._toggleClass(c,a):f.effects.animateClass.apply(this,[{toggle:c},a,b,d])},switchClass:function(c,a,b,d,e){return f.effects.animateClass.apply(this,[{add:a,remove:c},b,d,e])}});f.extend(f.effects,{version:"1.8.16",save:function(c,a){for(var b=0;b
    ").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),d=document.activeElement;c.wrap(b);if(c[0]===d||f.contains(c[0],d))f(d).focus();b=c.parent();if(c.css("position")=="static"){b.css({position:"relative"});c.css({position:"relative"})}else{f.extend(a,{position:c.css("position"),zIndex:c.css("z-index")});f.each(["top","left","bottom","right"],function(e,g){a[g]=c.css(g);if(isNaN(parseInt(a[g],10)))a[g]="auto"});c.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})}return b.css(a).show()},removeWrapper:function(c){var a,b=document.activeElement;if(c.parent().is(".ui-effects-wrapper")){a=c.parent().replaceWith(c);if(c[0]===b||f.contains(c[0],b))f(b).focus();return a}return c},setTransition:function(c,a,b,d){d=d||{};f.each(a,function(e,g){unit=c.cssUnit(g);if(unit[0]>0)d[g]=unit[0]*b+unit[1]});return d}});f.fn.extend({effect:function(c){var a=k.apply(this,arguments),b={options:a[1],duration:a[2],callback:a[3]};a=b.options.mode;var d=f.effects[c];if(f.fx.off||!d)return a?this[a](b.duration,b.callback):this.each(function(){b.callback&&b.callback.call(this)});return d.call(this,b)},_show:f.fn.show,show:function(c){if(l(c))return this._show.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="show";return this.effect.apply(this,a)}},_hide:f.fn.hide,hide:function(c){if(l(c))return this._hide.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="hide";return this.effect.apply(this,a)}},__toggle:f.fn.toggle,toggle:function(c){if(l(c)||typeof c==="boolean"||f.isFunction(c))return this.__toggle.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="toggle";return this.effect.apply(this,a)}},cssUnit:function(c){var a=this.css(c),b=[];f.each(["em","px","%","pt"],function(d,e){if(a.indexOf(e)>0)b=[parseFloat(a),e]});return b}});f.easing.jswing=f.easing.swing;f.extend(f.easing,{def:"easeOutQuad",swing:function(c,a,b,d,e){return f.easing[f.easing.def](c,a,b,d,e)},easeInQuad:function(c,a,b,d,e){return d*(a/=e)*a+b},easeOutQuad:function(c,a,b,d,e){return-d*(a/=e)*(a-2)+b},easeInOutQuad:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a+b;return-d/2*(--a*(a-2)-1)+b},easeInCubic:function(c,a,b,d,e){return d*(a/=e)*a*a+b},easeOutCubic:function(c,a,b,d,e){return d*((a=a/e-1)*a*a+1)+b},easeInOutCubic:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a+b;return d/2*((a-=2)*a*a+2)+b},easeInQuart:function(c,a,b,d,e){return d*(a/=e)*a*a*a+b},easeOutQuart:function(c,a,b,d,e){return-d*((a=a/e-1)*a*a*a-1)+b},easeInOutQuart:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a*a+b;return-d/2*((a-=2)*a*a*a-2)+b},easeInQuint:function(c,a,b,d,e){return d*(a/=e)*a*a*a*a+b},easeOutQuint:function(c,a,b,d,e){return d*((a=a/e-1)*a*a*a*a+1)+b},easeInOutQuint:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a*a*a+b;return d/2*((a-=2)*a*a*a*a+2)+b},easeInSine:function(c,a,b,d,e){return-d*Math.cos(a/e*(Math.PI/2))+d+b},easeOutSine:function(c,a,b,d,e){return d*Math.sin(a/e*(Math.PI/2))+b},easeInOutSine:function(c,a,b,d,e){return-d/2*(Math.cos(Math.PI*a/e)-1)+b},easeInExpo:function(c,a,b,d,e){return a==0?b:d*Math.pow(2,10*(a/e-1))+b},easeOutExpo:function(c,a,b,d,e){return a==e?b+d:d*(-Math.pow(2,-10*a/e)+1)+b},easeInOutExpo:function(c,a,b,d,e){if(a==0)return b;if(a==e)return b+d;if((a/=e/2)<1)return d/2*Math.pow(2,10*(a-1))+b;return d/2*(-Math.pow(2,-10*--a)+2)+b},easeInCirc:function(c,a,b,d,e){return-d*(Math.sqrt(1-(a/=e)*a)-1)+b},easeOutCirc:function(c,a,b,d,e){return d*Math.sqrt(1-(a=a/e-1)*a)+b},easeInOutCirc:function(c,a,b,d,e){if((a/=e/2)<1)return-d/2*(Math.sqrt(1-a*a)-1)+b;return d/2*(Math.sqrt(1-(a-=2)*a)+1)+b},easeInElastic:function(c,a,b,d,e){c=1.70158;var g=0,h=d;if(a==0)return b;if((a/=e)==1)return b+d;g||(g=e*0.3);if(h").css({position:"absolute",visibility:"visible",left:-f*(h/d),top:-e*(i/c)}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:h/d,height:i/c,left:g.left+f*(h/d)+(a.options.mode=="show"?(f-Math.floor(d/2))*(h/d):0),top:g.top+e*(i/c)+(a.options.mode=="show"?(e-Math.floor(c/2))*(i/c):0),opacity:a.options.mode=="show"?0:1}).animate({left:g.left+f*(h/d)+(a.options.mode=="show"?0:(f-Math.floor(d/2))*(h/d)),top:g.top+ -e*(i/c)+(a.options.mode=="show"?0:(e-Math.floor(c/2))*(i/c)),opacity:a.options.mode=="show"?1:0},a.duration||500);setTimeout(function(){a.options.mode=="show"?b.css({visibility:"visible"}):b.css({visibility:"visible"}).hide();a.callback&&a.callback.apply(b[0]);b.dequeue();j("div.ui-effects-explode").remove()},a.duration||500)})}})(jQuery);;(function(b){b.effects.fade=function(a){return this.queue(function(){var c=b(this),d=b.effects.setMode(c,a.options.mode||"hide");c.animate({opacity:d},{queue:false,duration:a.duration,easing:a.options.easing,complete:function(){a.callback&&a.callback.apply(this,arguments);c.dequeue()}})})}})(jQuery);;(function(c){c.effects.fold=function(a){return this.queue(function(){var b=c(this),j=["position","top","bottom","left","right"],d=c.effects.setMode(b,a.options.mode||"hide"),g=a.options.size||15,h=!!a.options.horizFirst,k=a.duration?a.duration/2:c.fx.speeds._default/2;c.effects.save(b,j);b.show();var e=c.effects.createWrapper(b).css({overflow:"hidden"}),f=d=="show"!=h,l=f?["width","height"]:["height","width"];f=f?[e.width(),e.height()]:[e.height(),e.width()];var i=/([0-9]+)%/.exec(g);if(i)g=parseInt(i[1],10)/100*f[d=="hide"?0:1];if(d=="show")e.css(h?{height:0,width:g}:{height:g,width:0});h={};i={};h[l[0]]=d=="show"?f[0]:g;i[l[1]]=d=="show"?f[1]:0;e.animate(h,k,a.options.easing).animate(i,k,a.options.easing,function(){d=="hide"&&b.hide();c.effects.restore(b,j);c.effects.removeWrapper(b);a.callback&&a.callback.apply(b[0],arguments);b.dequeue()})})}})(jQuery);;(function(b){b.effects.highlight=function(c){return this.queue(function(){var a=b(this),e=["backgroundImage","backgroundColor","opacity"],d=b.effects.setMode(a,c.options.mode||"show"),f={backgroundColor:a.css("backgroundColor")};if(d=="hide")f.opacity=0;b.effects.save(a,e);a.show().css({backgroundImage:"none",backgroundColor:c.options.color||"#ffff99"}).animate(f,{queue:false,duration:c.duration,easing:c.options.easing,complete:function(){d=="hide"&&a.hide();b.effects.restore(a,e);d=="show"&&!b.support.opacity&&this.style.removeAttribute("filter");c.callback&&c.callback.apply(this,arguments);a.dequeue()}})})}})(jQuery);;(function(d){d.effects.pulsate=function(a){return this.queue(function(){var b=d(this),c=d.effects.setMode(b,a.options.mode||"show");times=(a.options.times||5)*2-1;duration=a.duration?a.duration/2:d.fx.speeds._default/2;isVisible=b.is(":visible");animateTo=0;if(!isVisible){b.css("opacity",0).show();animateTo=1}if(c=="hide"&&isVisible||c=="show"&&!isVisible)times--;for(c=0;c').appendTo(document.body).addClass(a.options.className).css({top:d.top,left:d.left,height:b.innerHeight(),width:b.innerWidth(),position:"absolute"}).animate(c,a.duration,a.options.easing,function(){f.remove();a.callback&&a.callback.apply(b[0],arguments);b.dequeue()})})}})(jQuery);; \ No newline at end of file diff --git a/ckanext/reclinepreview/theme/public/vendor/slickgrid/jquery.event.drag-2.0.min.js b/ckanext/reclinepreview/theme/public/vendor/slickgrid/jquery.event.drag-2.0.min.js deleted file mode 100644 index 10a1c415655..00000000000 --- a/ckanext/reclinepreview/theme/public/vendor/slickgrid/jquery.event.drag-2.0.min.js +++ /dev/null @@ -1 +0,0 @@ -;(function(f){f.fn.drag=function(b,a,d){var e=typeof b=="string"?b:"",k=f.isFunction(b)?b:f.isFunction(a)?a:null;if(e.indexOf("drag")!==0)e="drag"+e;d=(b==k?a:d)||{};return k?this.bind(e,d,k):this.trigger(e)};var i=f.event,h=i.special,c=h.drag={defaults:{which:1,distance:0,not:":input",handle:null,relative:false,drop:true,click:false},datakey:"dragdata",livekey:"livedrag",add:function(b){var a=f.data(this,c.datakey),d=b.data||{};a.related+=1;if(!a.live&&b.selector){a.live=true;i.add(this,"draginit."+c.livekey,c.delegate)}f.each(c.defaults,function(e){if(d[e]!==undefined)a[e]=d[e]})},remove:function(){f.data(this,c.datakey).related-=1},setup:function(){if(!f.data(this,c.datakey)){var b=f.extend({related:0},c.defaults);f.data(this,c.datakey,b);i.add(this,"mousedown",c.init,b);this.attachEvent&&this.attachEvent("ondragstart",c.dontstart)}},teardown:function(){if(!f.data(this,c.datakey).related){f.removeData(this,c.datakey);i.remove(this,"mousedown",c.init);i.remove(this,"draginit",c.delegate);c.textselect(true);this.detachEvent&&this.detachEvent("ondragstart",c.dontstart)}},init:function(b){var a=b.data,d;if(!(a.which>0&&b.which!=a.which))if(!f(b.target).is(a.not))if(!(a.handle&&!f(b.target).closest(a.handle,b.currentTarget).length)){a.propagates=1;a.interactions=[c.interaction(this,a)];a.target=b.target;a.pageX=b.pageX;a.pageY=b.pageY;a.dragging=null;d=c.hijack(b,"draginit",a);if(a.propagates){if((d=c.flatten(d))&&d.length){a.interactions=[];f.each(d,function(){a.interactions.push(c.interaction(this,a))})}a.propagates=a.interactions.length;a.drop!==false&&h.drop&&h.drop.handler(b,a);c.textselect(false);i.add(document,"mousemove mouseup",c.handler,a);return false}}},interaction:function(b,a){return{drag:b,callback:new c.callback,droppable:[],offset:f(b)[a.relative?"position":"offset"]()||{top:0,left:0}}},handler:function(b){var a=b.data;switch(b.type){case!a.dragging&&"mousemove":if(Math.pow(b.pageX-a.pageX,2)+Math.pow(b.pageY-a.pageY,2)=0;f--)d[f]===e&&d.splice(f,1)};this.notify=function(e,f,g){for(var f=f||new G,g=g||this,A,ma=0;ma=this.fromRow&&d<=this.toRow&&e>=this.fromCell&&e<=this.toCell};this.toString=function(){return this.isSingleCell()?"("+this.fromRow+":"+this.fromCell+")":"("+this.fromRow+":"+this.fromCell+" - "+this.toRow+":"+this.toCell+")"}},NonDataRow:A,Group:ba,GroupTotals:g,EditorLock:f,GlobalEditorLock:new f}});ba.prototype= -new A;ba.prototype.equals=function(d){return this.value===d.value&&this.count===d.count&&this.collapsed===d.collapsed};g.prototype=new A})(jQuery);/* - - (c) 2009-2012 Michael Leibman - michael{dot}leibman{at}gmail{dot}com - http://github.com/mleibman/slickgrid - - Distributed under MIT license. - All rights reserved. - - SlickGrid v2.0 - - NOTES: - Cell/row DOM manipulations are done directly bypassing jQuery's DOM manipulation methods. - This increases the speed dramatically, but can only be done safely because there are no event handlers - or data associated with any cell/row DOM nodes. Cell editors must make sure they implement .destroy() - and do proper cleanup. -*/ -if("undefined"===typeof jQuery)throw"SlickGrid requires jquery module to be loaded";if(!jQuery.fn.drag)throw"SlickGrid requires jquery.event.drag module to be loaded";if("undefined"===typeof Slick)throw"slick.core.js not loaded"; -(function(e){e.extend(!0,window,{Slick:{Grid:function(ba,g,f,d){function fb(){if(!H){H=true;U=parseFloat(e.css(n[0],"width",true));Vb();ma(t);d.enableTextSelectionOnCells||v.bind("selectstart.ui",function(a){return e(a.target).is("input,textarea")});hb();Wb();ib();K();Xb();n.bind("resize.slickgrid",K);v.bind("scroll.slickgrid",jb);ca.bind("contextmenu.slickgrid",Yb).bind("click.slickgrid",Zb);Ka.bind("keydown.slickgrid",kb);B.bind("keydown.slickgrid",kb).bind("click.slickgrid",$b).bind("dblclick.slickgrid", -ac).bind("contextmenu.slickgrid",bc).bind("draginit",cc).bind("dragstart",dc).bind("drag",ec).bind("dragend",fc).delegate(".slick-cell","mouseenter",gc).delegate(".slick-cell","mouseleave",hc)}}function gb(a){for(var b=O.length;b>=0;b--)if(O[b]===a){O[b].destroy&&O[b].destroy();O.splice(b,1);break}}function Ub(){var a=e("
    ").appendTo("body"),b={width:a.width()-a[0].clientWidth,height:a.height()- -a[0].clientHeight};a.remove();return b}function Ja(a){for(var b=P,c=na?U-G.width:U,j=0,e=f.length;e--;)j=j+(f[e].width||La.width);P=d.fullWidthRows?Math.max(j,c):j;if(P!=b){B.width(P);da.width(P);lb=P>U-G.width}(P!=b||a)&&Ma()}function ma(a){a&&a.jquery&&a.attr("unselectable","on").css("MozUserSelect","none").bind("selectstart.ui",function(){return false})}function ic(){for(var a=1E6,b=e.browser.mozilla?5E6:1E9,c=e("
    ").appendTo(document.body);a<=b;){c.css("height",a+1E6); -if(c.height()!==a+1E6)break;else a=a+1E6}c.remove();return a}function Xb(){for(var a=B[0];(a=a.parentNode)!=document.body&&a!=null;)(a==v[0]||a.scrollWidth!=a.clientWidth||a.scrollHeight!=a.clientHeight)&&e(a).bind("scroll.slickgrid",mb)}function hb(){function a(){e(this).addClass("ui-state-hover")}function b(){e(this).removeClass("ui-state-hover")}t.empty();da.empty();I={};for(var c=0;c").html(""+j.name+"").width(j.width-V).attr("title",j.toolTip||j.name||"").data("fieldId",j.id).addClass(j.headerCssClass||"").appendTo(t);(d.enableColumnReorder||j.sortable)&&oa.hover(a,b);j.sortable&&oa.append("");d.showHeaderRow&&e("
    ").appendTo(da)}d.showHeaderRow&&e("
    ").appendTo(Q); -ta(r);nb();d.enableColumnReorder&&jc()}function Wb(){t.click(function(a){a.metaKey=a.metaKey||a.ctrlKey;if(!e(a.target).hasClass("slick-resizable-handle")){var b=e(a.target).closest(".slick-header-column");if(b.length){b=f[Na(b.data("fieldId"))];if(b.sortable&&p().commitCurrentEdit()){for(var c=null,j=0;j=g)){e(m);e("
    ").appendTo(m).bind("dragstart",function(g){if(!p().commitCurrentEdit())return false;c=g.pageX;e(this).parent().addClass("slick-header-column-active");var h=g=null;j.each(function(a,b){f[a].previousWidth=e(b).outerWidth()});if(d.forceFitColumns){h=g=0;for(a=k+1;a=0;a--){b=f[a];if(b.resizable){e=Math.max(b.minWidth||0,R);if(h&&b.previousWidth+h=0;a--){b=f[a];if(b.resizable)if(h&&b.maxWidth&&b.maxWidth-b.previousWidth
    ").appendTo(t);V=Pa=0;e.each(b,function(b,c){V=V+(parseFloat(a.css(c))||0)});e.each(c,function(b,c){Pa=Pa+(parseFloat(a.css(c))||0)});a.remove();var j=e("
    ").appendTo(B);a=e("").appendTo(j); -ua=va=0;e.each(b,function(b,c){ua=ua+(parseFloat(a.css(c))||0)});e.each(c,function(b,c){va=va+(parseFloat(a.css(c))||0)});j.remove();R=Math.max(V,ua)}function ib(){X=e("
    {{ _('Field') }}{{ _('Value') }}{{ _('Field') }}{{ _('Value') }}
    {{ _('Source') }}{{ _('Source') }} {{ h.link_to(pkg_dict.url, pkg_dict.url, rel='foaf:homepage', target='_blank') }}
    {{ _("Author") }}{{ _("Author") }} {{ h.mail_to(email_address=pkg_dict.author_email, name=pkg_dict.author) }}
    {{ _("Author") }}{{ _("Author") }} {{ pkg_dict.author }}
    {{ _('Maintainer') }}{{ _('Maintainer') }} {{ h.mail_to(email_address=pkg_dict.maintainer_email, name=pkg_dict.maintainer) }}
    {{ _('Maintainer') }}{{ _('Maintainer') }} {{ pkg_dict.maintainer }}
    {{ _("Version") }}{{ _("Version") }} {{ pkg_dict.version }}
    {{ _("State") }}{{ _("State") }} {{ pkg_dict.state }}
    {{ _(key) }}{{ _(key) }} {{ value }}