diff --git a/ckan/config/deployment.ini_tmpl b/ckan/config/deployment.ini_tmpl index f9d25c4eed9..0cad20aaac2 100644 --- a/ckan/config/deployment.ini_tmpl +++ b/ckan/config/deployment.ini_tmpl @@ -111,6 +111,11 @@ ckan.site_url = ## Favicon (default is the CKAN software favicon) ckan.favicon = /images/icons/ckan.ico +## The gravatar default to use. This can be any of the pre-defined strings +## as defined on http://en.gravatar.com/site/implement/images/ (e.g. "identicon" +## or "mm"). Or it can be a url, e.g. "http://example.com/images/avatar.jpg" +ckan.gravatar_default = identicon + ## Solr support #solr_url = http://127.0.0.1:8983/solr diff --git a/ckan/controllers/group.py b/ckan/controllers/group.py index bcf064c2d40..b7f8efe2413 100644 --- a/ckan/controllers/group.py +++ b/ckan/controllers/group.py @@ -18,7 +18,6 @@ log = logging.getLogger(__name__) - class GroupController(BaseController): ## hooks for subclasses @@ -244,28 +243,14 @@ def edit(self, id, data=None, errors=None, error_summary=None): def _get_group_type(self, id): """ - Given the id of a group it determines the plugin to load - based on the group's type name (type). The plugin found - will be returned, or None if there is no plugin associated with - the type. - - Uses a minimal context to do so. The main use of this method - is for figuring out which plugin to delegate to. - - aborts if an exception is raised. + Given the id of a group it determines the type of a group given + a valid id/name for the group. """ - global _controller_behaviour_for - - context = {'model': model, 'session': model.Session, - 'user': c.user or c.author} - try: - data = get_action('group_show')(context, {'id': id}) - except NotFound: - abort(404, _('Group not found')) - except NotAuthorized: - abort(401, _('Unauthorized to read group %s') % id) - return data['type'] + group = model.Group.get( id ) + if not group: + return None + return group.type def _save_new(self, context, group_type=None): try: diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py index 9504f765d90..dd9e7f358ef 100644 --- a/ckan/controllers/package.py +++ b/ckan/controllers/package.py @@ -325,6 +325,7 @@ def new(self, data=None, errors=None, error_summary=None): errors = errors or {} error_summary = error_summary or {} vars = {'data': data, 'errors': errors, 'error_summary': error_summary} + c.errors_json = json.dumps(errors) self._setup_template_variables(context, {'id': id}) @@ -370,6 +371,7 @@ def edit(self, id, data=None, errors=None, error_summary=None): errors = errors or {} vars = {'data': data, 'errors': errors, 'error_summary': error_summary} + c.errors_json = json.dumps(errors) self._setup_template_variables(context, {'id': id}, package_type=package_type) @@ -450,8 +452,6 @@ def _get_package_type(self, id): aborts if an exception is raised. """ - global _controller_behaviour_for - context = {'model': model, 'session': model.Session, 'user': c.user or c.author} try: diff --git a/ckan/controllers/user.py b/ckan/controllers/user.py index b7f74533e7b..f446c21c790 100644 --- a/ckan/controllers/user.py +++ b/ckan/controllers/user.py @@ -11,7 +11,7 @@ from ckan.logic import NotFound, NotAuthorized, ValidationError from ckan.logic import check_access, get_action from ckan.logic import tuplize_dict, clean_dict, parse_params -from ckan.logic.schema import user_new_form_schema, user_edit_form_schema +from ckan.logic.schema import user_new_form_schema, user_edit_form_schema from ckan.logic.action.get import user_activity_list_html from ckan.lib.captcha import check_recaptcha, CaptchaError @@ -28,7 +28,7 @@ def __before__(self, action, **env): if c.action not in ('login','request_reset','perform_reset',): abort(401, _('Not authorized to see this page')) - ## hooks for subclasses + ## hooks for subclasses new_user_form = 'user/new_user_form.html' edit_user_form = 'user/edit_user_form.html' @@ -207,10 +207,10 @@ def edit(self, id=None, data=None, errors=None, error_summary=None): abort(404, _('User not found')) user_obj = context.get('user_obj') - + if not (ckan.authz.Authorizer().is_sysadmin(unicode(c.user)) or c.user == user_obj.name): abort(401, _('User %s not authorized to edit %s') % (str(c.user), id)) - + errors = errors or {} vars = {'data': data, 'errors': errors, 'error_summary': error_summary} @@ -255,7 +255,7 @@ def login(self): return render('user/login.html') else: return render('user/logout_first.html') - + def logged_in(self): if c.user: context = {'model': model, @@ -277,14 +277,14 @@ def logged_in(self): h.flash_error('Login failed. Bad username or password.' + \ ' (Or if using OpenID, it hasn\'t been associated with a user account.)') h.redirect_to(controller='user', action='login') - + def logged_out(self): c.user = None response.delete_cookie("ckan_user") response.delete_cookie("ckan_display_name") response.delete_cookie("ckan_apikey") return render('user/logout.html') - + def request_reset(self): if request.method == 'POST': id = request.params.get('user') @@ -346,7 +346,7 @@ def perform_reset(self, id): if request.method == 'POST': try: - context['reset_password'] = True + context['reset_password'] = True new_password = self._get_form_password() user_dict['password'] = new_password user_dict['reset_key'] = c.reset_key @@ -374,7 +374,7 @@ def _format_about(self, about): log.error('Could not print "about" field Field: %r Error: %r', about, e) html = _('Error: Could not parse About text') return html - + def _get_form_password(self): password1 = request.params.getone('password1') password2 = request.params.getone('password2') @@ -384,4 +384,4 @@ def _get_form_password(self): elif not password1 == password2: raise ValueError(_("The passwords you entered do not match.")) return password1 - + diff --git a/ckan/lib/alphabet_paginate.py b/ckan/lib/alphabet_paginate.py index f30f5f9c78d..0afa88f3ff1 100644 --- a/ckan/lib/alphabet_paginate.py +++ b/ckan/lib/alphabet_paginate.py @@ -45,11 +45,14 @@ def __init__(self, collection, alpha_attribute, page, other_text, paging_thresho self.controller_name = controller_name self.available = dict( (c,0,) for c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" ) for c in self.collection: - x = c[0] if isinstance( c, unicode ) else getattr(c, self.alpha_attribute)[0] + if isinstance(c, unicode): + x = c[0] + elif isinstance(c, dict): + x = c[self.alpha_attribute][0] + else: + x = getattr(c, self.alpha_attribute)[0] self.available[x] = self.available.get(x, 0) + 1 - - def pager(self, q=None): '''Returns pager html - for navigating between the pages. e.g. Something like this: diff --git a/ckan/lib/authenticator.py b/ckan/lib/authenticator.py index 8eea54ae347..b56711a3427 100644 --- a/ckan/lib/authenticator.py +++ b/ckan/lib/authenticator.py @@ -5,7 +5,7 @@ class OpenIDAuthenticator(object): implements(IAuthenticator) - + def authenticate(self, environ, identity): if 'repoze.who.plugins.openid.userid' in identity: openid = identity.get('repoze.who.plugins.openid.userid') @@ -15,16 +15,16 @@ def authenticate(self, environ, identity): else: return user.name return None - + class UsernamePasswordAuthenticator(object): implements(IAuthenticator) - + def authenticate(self, environ, identity): if not 'login' in identity or not 'password' in identity: return None user = User.by_name(identity.get('login')) - if user is None: + if user is None: return None if user.validate_password(identity.get('password')): return user.name diff --git a/ckan/lib/create_test_data.py b/ckan/lib/create_test_data.py index b90fbc3bd3e..be6cb04f446 100644 --- a/ckan/lib/create_test_data.py +++ b/ckan/lib/create_test_data.py @@ -22,7 +22,7 @@ class CreateTestData(cli.CkanCommand): tag_names = [] group_names = set() user_refs = [] - + pkg_core_fields = ['name', 'title', 'version', 'url', 'notes', 'author', 'author_email', 'maintainer', 'maintainer_email', @@ -89,7 +89,7 @@ def create_test_user(cls): @classmethod def create_arbitrary(cls, package_dicts, relationships=[], - extra_user_names=[], extra_group_names=[], + extra_user_names=[], extra_group_names=[], admins=[]): '''Creates packages and a few extra objects as well at the same time if required. @@ -101,7 +101,7 @@ def create_arbitrary(cls, package_dicts, relationships=[], @param extra_group_names - a list of group names to create. No properties get set though. @param admins - a list of user names to make admins of all the - packages created. + packages created. ''' assert isinstance(relationships, (list, tuple)) assert isinstance(extra_user_names, (list, tuple)) @@ -111,11 +111,11 @@ def create_arbitrary(cls, package_dicts, relationships=[], new_user_names = extra_user_names new_group_names = set() new_groups = {} - - rev = model.repo.new_revision() + + rev = model.repo.new_revision() rev.author = cls.author rev.message = u'Creating test packages.' - + admins_list = defaultdict(list) # package_name: admin_names if package_dicts: if isinstance(package_dicts, dict): @@ -131,7 +131,7 @@ def create_arbitrary(cls, package_dicts, relationships=[], if isinstance(val, str): val = unicode(val) if attr=='name': - continue + continue if attr in cls.pkg_core_fields: pass elif attr == 'download_url': @@ -160,7 +160,7 @@ def create_arbitrary(cls, package_dicts, relationships=[], if not tag: tag = model.Tag(name=tag_name) cls.tag_names.append(tag_name) - model.Session.add(tag) + model.Session.add(tag) pkg.add_tag(tag) model.Session.flush() elif attr == 'groups': @@ -208,8 +208,8 @@ def create_arbitrary(cls, package_dicts, relationships=[], model.repo.commit_and_remove() needs_commit = False - - rev = model.repo.new_revision() + + rev = model.repo.new_revision() for group_name in extra_group_names: group = model.Group(name=unicode(group_name)) model.Session.add(group) @@ -258,7 +258,7 @@ def create_arbitrary(cls, package_dicts, relationships=[], needs_commit = False if relationships: - rev = model.repo.new_revision() + rev = model.repo.new_revision() rev.author = cls.author rev.message = u'Creating package relationships.' @@ -270,7 +270,7 @@ def pkg(pkg_name): needs_commit = True model.repo.commit_and_remove() - + @classmethod def create_groups(cls, group_dicts, admin_user_name=None, auth_profile=""): @@ -324,7 +324,7 @@ def create(cls, auth_profile="", package_type=None): cls.pkg_names = [u'annakarenina', u'warandpeace'] pkg1 = model.Package(name=cls.pkg_names[0], type=package_type) - if auth_profile == "publisher": + if auth_profile == "publisher": pkg1.group = publisher_group model.Session.add(pkg1) pkg1.title = u'A Novel By Tolstoy' @@ -368,7 +368,7 @@ def create(cls, auth_profile="", package_type=None): u with umlaut \xfc 66-style quote \u201c foreign word: th\xfcmb - + Needs escaping: left arrow < @@ -379,7 +379,7 @@ def create(cls, auth_profile="", package_type=None): tag1 = model.Tag(name=u'russian') tag2 = model.Tag(name=u'tolstoy') - if auth_profile == "publisher": + if auth_profile == "publisher": pkg2.group = publisher_group # Flexible tag, allows spaces, upper-case, @@ -407,12 +407,12 @@ def create(cls, auth_profile="", package_type=None): type=auth_profile or 'group') for obj in [david, roger]: model.Session.add(obj) - + cls.group_names.add(u'david') cls.group_names.add(u'roger') model.Session.flush() - + model.Session.add(model.Member(table_id=pkg1.id, table_name='package', group=david)) model.Session.add(model.Member(table_id=pkg2.id, table_name='package', group=david)) model.Session.add(model.Member(table_id=pkg1.id, table_name='package', group=roger)) @@ -447,7 +447,7 @@ def create(cls, auth_profile="", package_type=None): # Create a couple of authorization groups for ag_name in [u'anauthzgroup', u'anotherauthzgroup']: - ag=model.AuthorizationGroup.by_name(ag_name) + ag=model.AuthorizationGroup.by_name(ag_name) if not ag: #may already exist, if not create ag=model.AuthorizationGroup(name=ag_name) model.Session.add(ag) @@ -559,7 +559,7 @@ def get_all_data(cls): 'groups':'ukgov test1 test2 penguin', 'license':'odc-by', 'notes':u'''From - + > The Government Information Locator Service (GILS) is an effort to identify, locate, and describe publicly available Federal > Because this collection is decentralized, the GPO @@ -604,7 +604,7 @@ def get_all_data(cls): {'name':'uk-government-expenditure', 'title':'UK Government Expenditure', 'tags':'workshop-20081101,uk,gov,expenditure,finance,public,funding,penguin'.split(','), - 'groups':'ukgov penguin', + 'groups':'ukgov penguin', 'notes':'''Discussed at [Workshop on Public Information, 2008-11-02](http://okfn.org/wiki/PublicInformation). Overview is available in Red Book, or Financial Statement and Budget Report (FSBR), [published by the Treasury](http://www.hm-treasury.gov.uk/budget.htm).''', @@ -613,7 +613,7 @@ def get_all_data(cls): {'name':'se-publications', 'title':'Sweden - Government Offices of Sweden - Publications', 'url':'http://www.sweden.gov.se/sb/d/574', - 'groups':'penguin', + 'groups':'penguin', 'tags':u'country-sweden,format-pdf,access-www,documents,publications,government,eutransparency,penguin,CAPITALS,surprise.,greek omega \u03a9,japanese katakana \u30a1'.split(','), 'license':'', 'notes':'''### About @@ -627,7 +627,7 @@ def get_all_data(cls): }, {'name':'se-opengov', 'title':'Opengov.se', - 'groups':'penguin', + 'groups':'penguin', 'url':'http://www.opengov.se/', 'download_url':'http://www.opengov.se/data/open/', 'tags':'country-sweden,government,data,penguin'.split(','), diff --git a/ckan/lib/dictization/__init__.py b/ckan/lib/dictization/__init__.py index 66d4abf6dec..42f748b07d6 100644 --- a/ckan/lib/dictization/__init__.py +++ b/ckan/lib/dictization/__init__.py @@ -3,7 +3,7 @@ import sqlalchemy from pylons import config -# NOTE +# NOTE # The functions in this file contain very generic methods for dictizing objects # and saving dictized objects. If a specialised use is needed please do NOT extend # these functions. Copy code from here as needed. @@ -68,7 +68,7 @@ def obj_list_dictize(obj_list, context, sort_key=lambda x:x): return sorted(result_list, key=sort_key) def obj_dict_dictize(obj_dict, context, sort_key=lambda x:x): - '''Get a dict whose values are model objects + '''Get a dict whose values are model objects and represent it as a list of dicts''' result_list = [] @@ -93,7 +93,7 @@ def get_unique_constraints(table, context): def table_dict_save(table_dict, ModelClass, context): '''Given a dict and a model class, update or create a sqlalchemy object. - This will use an existing object if "id" is supplied OR if any unique + This will use an existing object if "id" is supplied OR if any unique constraints are met. e.g supplying just a tag name will get out that tag obj. ''' @@ -107,7 +107,7 @@ def table_dict_save(table_dict, ModelClass, context): unique_constriants = get_unique_constraints(table, context) id = table_dict.get("id") - + if id: obj = session.query(ModelClass).get(id) diff --git a/ckan/lib/dictization/model_dictize.py b/ckan/lib/dictization/model_dictize.py index ee3d268250c..39e36b27ed1 100644 --- a/ckan/lib/dictization/model_dictize.py +++ b/ckan/lib/dictization/model_dictize.py @@ -12,7 +12,7 @@ ## package save -def group_list_dictize(obj_list, context, +def group_list_dictize(obj_list, context, sort_key=lambda x:x['display_name'], reverse=False): active = context.get('active', True) @@ -93,10 +93,10 @@ def _execute_with_revision(q, rev_table, context): But you can provide revision_id, revision_date or pending in the context and it will filter to an earlier time or the latest unmoderated object revision. - + Raises NotFound if context['revision_id'] is provided, but the revision ID does not exist. - + Returns [] if there are no results. ''' @@ -113,7 +113,7 @@ def _execute_with_revision(q, rev_table, context): if not revision: raise NotFound revision_date = revision.timestamp - + if revision_date: q = q.where(rev_table.c.revision_timestamp <= revision_date) q = q.where(rev_table.c.expired_timestamp > revision_date) @@ -133,7 +133,7 @@ def package_dictize(pkg, context): but you can provide revision_id, revision_date or pending in the context and it will filter to an earlier time or the latest unmoderated object revision. - + May raise NotFound. TODO: understand what the specific set of circumstances are that cause this. ''' @@ -148,7 +148,7 @@ def package_dictize(pkg, context): #resources res_rev = model.resource_revision_table resource_group = model.resource_group_table - q = select([res_rev], from_obj = res_rev.join(resource_group, + q = select([res_rev], from_obj = res_rev.join(resource_group, resource_group.c.id == res_rev.c.resource_group_id)) q = q.where(resource_group.c.package_id == pkg.id) result = _execute_with_revision(q, res_rev, context) @@ -156,7 +156,7 @@ def package_dictize(pkg, context): #tags tag_rev = model.package_tag_revision_table tag = model.tag_table - q = select([tag, tag_rev.c.state, tag_rev.c.revision_timestamp], + q = select([tag, tag_rev.c.state, tag_rev.c.revision_timestamp], from_obj=tag_rev.join(tag, tag.c.id == tag_rev.c.tag_id) ).where(tag_rev.c.package_id == pkg.id) result = _execute_with_revision(q, tag_rev, context) @@ -171,7 +171,8 @@ def package_dictize(pkg, context): group = model.group_table q = select([group], from_obj=member_rev.join(group, group.c.id == member_rev.c.group_id) - ).where(member_rev.c.table_id == pkg.id) + ).where(member_rev.c.table_id == pkg.id)\ + .where(member_rev.c.state == 'active') result = _execute_with_revision(q, member_rev, context) result_dict["groups"] = obj_list_dictize(result, context) #relations @@ -182,7 +183,7 @@ def package_dictize(pkg, context): q = select([rel_rev]).where(rel_rev.c.object_package_id == pkg.id) result = _execute_with_revision(q, rel_rev, context) result_dict["relationships_as_object"] = obj_list_dictize(result, context) - + # Extra properties from the domain object # We need an actual Package object for this, not a PackageRevision if isinstance(pkg,PackageRevision): @@ -279,9 +280,10 @@ def tag_dictize(tag, context): result_dict = table_dictize(tag, context) result_dict["packages"] = obj_list_dictize(tag.packages, context) + return result_dict -def user_list_dictize(obj_list, context, +def user_list_dictize(obj_list, context, sort_key=lambda x:x['name'], reverse=False): result_list = [] @@ -302,13 +304,13 @@ def user_dictize(user, context): result_dict = table_dictize(user, context) del result_dict['password'] - + result_dict['display_name'] = user.display_name result_dict['email_hash'] = user.email_hash result_dict['number_of_edits'] = user.number_of_edits() result_dict['number_administered_packages'] = user.number_administered_packages() - return result_dict + return result_dict def task_status_dictize(task_status, context): return table_dictize(task_status, context) @@ -316,23 +318,23 @@ def task_status_dictize(task_status, context): ## conversion to api def group_to_api1(group, context): - + dictized = group_dictize(group, context) - dictized["extras"] = dict((extra["key"], json.loads(extra["value"])) + dictized["extras"] = dict((extra["key"], json.loads(extra["value"])) for extra in dictized["extras"]) dictized["packages"] = sorted([package["name"] for package in dictized["packages"]]) return dictized def group_to_api2(group, context): - + dictized = group_dictize(group, context) - dictized["extras"] = dict((extra["key"], json.loads(extra["value"])) + dictized["extras"] = dict((extra["key"], json.loads(extra["value"])) for extra in dictized["extras"]) dictized["packages"] = sorted([package["id"] for package in dictized["packages"]]) return dictized def tag_to_api1(tag, context): - + dictized = tag_dictize(tag, context) return sorted([package["name"] for package in dictized["packages"]]) diff --git a/ckan/lib/dictization/model_save.py b/ckan/lib/dictization/model_save.py index f4ee8a0c9e1..77d20ad2b10 100644 --- a/ckan/lib/dictization/model_save.py +++ b/ckan/lib/dictization/model_save.py @@ -8,7 +8,7 @@ def resource_dict_save(res_dict, context): model = context["model"] session = context["session"] trigger_url_change = False - + id = res_dict.get("id") obj = None if id: @@ -21,7 +21,7 @@ def resource_dict_save(res_dict, context): table = class_mapper(model.Resource).mapped_table fields = [field.name for field in table.c] - + for key, value in res_dict.iteritems(): if isinstance(value, list): continue @@ -68,7 +68,7 @@ def package_resource_list_save(res_dicts, package, context): else: resource.state = 'deleted' resource_list.append(resource) - tag_package_tag = dict((package_tag.tag, package_tag) + tag_package_tag = dict((package_tag.tag, package_tag) for package_tag in package.package_tag_all) @@ -89,7 +89,7 @@ def package_extras_save(extra_dicts, obj, context): for extra_dict in extra_dicts: if extra_dict.get("deleted"): continue - + if extra_dict['value'] is None: pass elif extras_as_string: @@ -146,10 +146,10 @@ def package_tag_list_save(tag_dicts, package, context): session = context["session"] pending = context.get('pending') - tag_package_tag = dict((package_tag.tag, package_tag) + tag_package_tag = dict((package_tag.tag, package_tag) for package_tag in package.package_tag_all) - + tag_package_tag_inactive = dict( [ (tag,pt) for tag,pt in tag_package_tag.items() if pt.state in ['deleted', 'pending-deleted'] ] @@ -199,7 +199,7 @@ def package_membership_list_save(group_dicts, package, context): members = session.query(model.Member).filter_by(table_id = package.id) - group_member = dict((member.group, member) + group_member = dict((member.group, member) for member in members) groups = set() @@ -233,7 +233,7 @@ def package_membership_list_save(group_dicts, package, context): member_obj.state = 'active' session.add(member_obj) - + def relationship_list_save(relationship_dicts, package, attr, context): allow_partial_update = context.get("allow_partial_update", False) @@ -248,7 +248,7 @@ def relationship_list_save(relationship_dicts, package, attr, context): relationships = [] for relationship_dict in relationship_dicts: - obj = table_dict_save(relationship_dict, + obj = table_dict_save(relationship_dict, model.PackageRelationship, context) relationships.append(obj) @@ -263,7 +263,7 @@ def relationship_list_save(relationship_dicts, package, attr, context): def package_dict_save(pkg_dict, context): import uuid - + model = context["model"] package = context.get("package") allow_partial_update = context.get("allow_partial_update", False) @@ -319,13 +319,21 @@ def group_member_save(context, group_dict, member_table_name): group_id=group.id, ).all() - entity_member = dict(((member.table_id, member.capacity), member) for member in members) + processed = { + 'added': [], + 'removed': [] + } + entity_member = dict(((member.table_id, member.capacity), member) for member in members) for entity_id in set(entity_member.keys()) - set(entities.keys()): + if entity_member[entity_id].state != 'deleted': + processed['removed'].append(entity_id[0]) entity_member[entity_id].state = 'deleted' session.add(entity_member[entity_id]) for entity_id in set(entity_member.keys()) & set(entities.keys()): + if entity_member[entity_id].state != 'active': + processed['added'].append(entity_id[0]) entity_member[entity_id].state = 'active' session.add(entity_member[entity_id]) @@ -333,12 +341,16 @@ def group_member_save(context, group_dict, member_table_name): member = Member(group=group, group_id=group.id, table_id=entity_id[0], table_name=member_table_name[:-1], capacity=entity_id[1]) + processed['added'].append(entity_id[0]) session.add(member) + return processed + def group_dict_save(group_dict, context): - import uuid - + from ckan.lib.search import rebuild + import uuid + model = context["model"] session = context["session"] group = context.get("group") @@ -346,19 +358,27 @@ def group_dict_save(group_dict, context): Group = model.Group if group: - group_dict["id"] = group.id + group_dict["id"] = group.id group = table_dict_save(group_dict, Group, context) if not group.id: group.id = str(uuid.uuid4()) - + context['group'] = group - group_member_save(context, group_dict, 'packages') + pkgs_edited = group_member_save(context, group_dict, 'packages') group_member_save(context, group_dict, 'users') group_member_save(context, group_dict, 'groups') group_member_save(context, group_dict, 'tags') + # We will get a list of packages that we have either added or + # removed from the group, and trigger a re-index. + package_ids = pkgs_edited['removed'] + package_ids.extend( pkgs_edited['added'] ) + if package_ids: + session.commit() + map( rebuild, package_ids ) + extras = group_extras_save(group_dict.get("extras", {}), context) if extras or not allow_partial_update: old_extras = set(group.extras.keys()) @@ -366,8 +386,7 @@ def group_dict_save(group_dict, context): for key in old_extras - new_extras: del group.extras[key] for key in new_extras: - group.extras[key] = extras[key] - + group.extras[key] = extras[key] return group @@ -377,11 +396,11 @@ def user_dict_save(user_dict, context): model = context['model'] session = context['session'] user = context.get('user_obj') - + User = model.User if user: user_dict['id'] = user.id - + if 'password' in user_dict and not len(user_dict['password']): del user_dict['password'] @@ -409,7 +428,7 @@ def package_api_to_dict(api1_dict, context): updated_extras.update(value) new_value = [] - + for extras_key, extras_value in updated_extras.iteritems(): if extras_value is not None: new_value.append({"key": extras_key, @@ -430,7 +449,7 @@ def package_api_to_dict(api1_dict, context): dictized["resources"] = [{'url': download_url}] download_url = dictized.pop('download_url', None) - + return dictized def group_api_to_dict(api1_dict, context): @@ -442,7 +461,7 @@ def group_api_to_dict(api1_dict, context): if key == 'packages': new_value = [{"id": item} for item in value] if key == 'extras': - new_value = [{"key": extra_key, "value": value[extra_key]} + new_value = [{"key": extra_key, "value": value[extra_key]} for extra_key in value] dictized[key] = new_value @@ -453,7 +472,7 @@ def task_status_dict_save(task_status_dict, context): task_status = context.get("task_status") allow_partial_update = context.get("allow_partial_update", False) if task_status: - task_status_dict["id"] = task_status.id + task_status_dict["id"] = task_status.id task_status = table_dict_save(task_status_dict, model.TaskStatus, context) return task_status diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py index 1e7004f8521..82705d032c5 100644 --- a/ckan/lib/helpers.py +++ b/ckan/lib/helpers.py @@ -5,8 +5,10 @@ Consists of functions to typically be used within templates, but also available to Controllers. This module is available to templates as 'h'. """ +import email.utils import datetime import re +import urllib from webhelpers.html import escape, HTML, literal, url_escape from webhelpers.html.tools import mail_to @@ -329,14 +331,23 @@ def icon_html(url, alt=None): def icon(name, alt=None): return icon_html(icon_url(name),alt) -def linked_gravatar(email_hash, size=100, default="identicon"): +def linked_gravatar(email_hash, size=100, default=None): return literal(''' %s''' % gravatar(email_hash,size,default) ) -def gravatar(email_hash, size=100, default="identicon"): +_VALID_GRAVATAR_DEFAULTS = ['404', 'mm', 'identicon', 'monsterid', 'wavatar', 'retro'] +def gravatar(email_hash, size=100, default=None): + if default is None: + from pylons import config + default = config.get('ckan.gravatar_default', 'identicon') + + if not default in _VALID_GRAVATAR_DEFAULTS: + # treat the default as a url + default = urllib.quote(default, safe='') + return literal('''''' % (email_hash, size, default) @@ -397,6 +408,27 @@ def date_str_to_datetime(date_str): # a strptime. Also avoids problem with Python 2.5 not having %f. return datetime.datetime(*map(int, re.split('[^\d]', date_str))) +def parse_rfc_2822_date(date_str, tz_aware=True): + """ + Parse a date string of the form specified in RFC 2822, and return a datetime. + + RFC 2822 is the date format used in HTTP headers. + + If the date string contains a timezone indication, and tz_aware is True, + then the associated tzinfo is attached to the returned datetime object. + + Returns None if the string cannot be parse as a valid datetime. + """ + time_tuple = email.utils.parsedate_tz(date_str) + + if not time_tuple: + return None + + if not tz_aware: + time_tuple = time_tuple[:-1] + (None,) + + return datetime.datetime.fromtimestamp(email.utils.mktime_tz(time_tuple)) + def time_ago_in_words_from_str(date_str, granularity='month'): if date_str: return date.time_ago_in_words(date_str_to_datetime(date_str), granularity=granularity) diff --git a/ckan/lib/plugins.py b/ckan/lib/plugins.py index 619fa02f4eb..1e20a8c0aac 100644 --- a/ckan/lib/plugins.py +++ b/ckan/lib/plugins.py @@ -54,8 +54,11 @@ def register_package_plugins(map): exception will be raised. """ global _default_package_plugin - if _default_package_plugin: - # we've already set things up + + # This function should have not effect if called more than once. + # This should not occur in normal deployment, but it may happen when + # running unit tests. + if _default_package_plugin is not None: return # Create the mappings and register the fallback behaviour if one is found. @@ -101,6 +104,12 @@ def register_group_plugins(map): """ global _default_group_plugin + # This function should have not effect if called more than once. + # This should not occur in normal deployment, but it may happen when + # running unit tests. + if _default_group_plugin is not None: + return + # Create the mappings and register the fallback behaviour if one is found. for plugin in plugins.PluginImplementations(plugins.IGroupForm): if plugin.is_fallback(): diff --git a/ckan/lib/search/index.py b/ckan/lib/search/index.py index d3e74aef537..b6c376c94d6 100644 --- a/ckan/lib/search/index.py +++ b/ckan/lib/search/index.py @@ -16,7 +16,7 @@ PACKAGE_TYPE = "package" KEY_CHARS = string.digits + string.letters + "_-" SOLR_FIELDS = [TYPE_FIELD, "res_url", "text", "urls", "indexed_ts", "site_id"] -RESERVED_FIELDS = SOLR_FIELDS + ["tags", "groups", "res_description", +RESERVED_FIELDS = SOLR_FIELDS + ["tags", "groups", "res_description", "res_format", "res_url"] RELATIONSHIP_TYPES = PackageRelationship.types @@ -39,28 +39,28 @@ def clear_index(): conn.close() class SearchIndex(object): - """ - A search index handles the management of documents of a specific type in the - index, but no queries. - The default implementation maps many of the methods, so most subclasses will - only have to implement ``update_dict`` and ``remove_dict``. - """ - + """ + A search index handles the management of documents of a specific type in the + index, but no queries. + The default implementation maps many of the methods, so most subclasses will + only have to implement ``update_dict`` and ``remove_dict``. + """ + def __init__(self): pass - + def insert_dict(self, data): """ Insert new data from a dictionary. """ return self.update_dict(data) - + def update_dict(self, data): """ Update data from a dictionary. """ log.debug("NOOP Index: %s" % ",".join(data.keys())) - + def remove_dict(self, data): """ Delete an index entry uniquely identified by ``data``. """ log.debug("NOOP Delete: %s" % ",".join(data.keys())) - + def clear(self): """ Delete the complete index. """ clear_index() @@ -68,26 +68,26 @@ def clear(self): def get_all_entity_ids(self): """ Return a list of entity IDs in the index. """ raise NotImplemented - + class NoopSearchIndex(SearchIndex): pass class PackageSearchIndex(SearchIndex): def remove_dict(self, pkg_dict): self.delete_package(pkg_dict) - + def update_dict(self, pkg_dict): self.index_package(pkg_dict) def index_package(self, pkg_dict): - if pkg_dict is None: - return + if pkg_dict is None: + return if (not pkg_dict.get('state')) or ('active' not in pkg_dict.get('state')): return self.delete_package(pkg_dict) conn = make_connection() index_fields = RESERVED_FIELDS + pkg_dict.keys() - + # include the extras in the main namespace extras = pkg_dict.get('extras', {}) for (key, value) in extras.items(): @@ -100,7 +100,7 @@ def index_package(self, pkg_dict): if 'extras' in pkg_dict: del pkg_dict['extras'] - # flatten the structure for indexing: + # flatten the structure for indexing: for resource in pkg_dict.get('resources', []): for (okey, nkey) in [('description', 'res_description'), ('format', 'res_format'), @@ -108,18 +108,18 @@ def index_package(self, pkg_dict): pkg_dict[nkey] = pkg_dict.get(nkey, []) + [resource.get(okey, u'')] if 'resources' in pkg_dict: del pkg_dict['resources'] - + # index relationships as : rel_dict = {} rel_types = list(itertools.chain(RELATIONSHIP_TYPES)) for rel in pkg_dict.get('relationships', []): _type = rel.get('type', 'rel') - if (_type in pkg_dict.keys()) or (_type not in rel_types): + if (_type in pkg_dict.keys()) or (_type not in rel_types): continue rel_dict[_type] = rel_dict.get(_type, []) + [rel.get('object')] - + pkg_dict.update(rel_dict) - + if 'relationships' in pkg_dict: del pkg_dict['relationships'] @@ -134,7 +134,7 @@ def index_package(self, pkg_dict): # mark this CKAN instance as data source: pkg_dict['site_id'] = config.get('ckan.site_id') - + # add a unique index_id to avoid conflicts import hashlib pkg_dict['index_id'] = hashlib.md5('%s%s' % (pkg_dict['id'],config.get('ckan.site_id'))).hexdigest() @@ -144,7 +144,7 @@ def index_package(self, pkg_dict): assert pkg_dict, 'Plugin must return non empty package dict on index' - # send to solr: + # send to solr: try: conn.add_many([pkg_dict]) conn.commit(wait_flush=False, wait_searcher=False) @@ -152,8 +152,8 @@ def index_package(self, pkg_dict): log.exception(e) raise SearchIndexError(e) finally: - conn.close() - + conn.close() + log.debug("Updated index for %s" % pkg_dict.get('name')) def delete_package(self, pkg_dict): diff --git a/ckan/logic/__init__.py b/ckan/logic/__init__.py index 250d93d4cde..513f2918ab3 100644 --- a/ckan/logic/__init__.py +++ b/ckan/logic/__init__.py @@ -25,6 +25,11 @@ class ActionError(Exception): def __init__(self, extra_msg=None): self.extra_msg = extra_msg + def __str__(self): + err_msgs = (super(ActionError, self).__str__(), + self.extra_msg) + return ' - '.join([str(err_msg) for err_msg in err_msgs if err_msg]) + class NotFound(ActionError): pass @@ -40,6 +45,11 @@ def __init__(self, error_dict, error_summary=None, extra_msg=None): self.error_summary = error_summary self.extra_msg = extra_msg + def __str__(self): + err_msgs = (super(ValidationError, self).__str__(), + self.error_summary) + return ' - '.join([str(err_msg) for err_msg in err_msgs if err_msg]) + log = logging.getLogger(__name__) def parse_params(params, ignore_keys=None): diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index a148b6a8071..71bd7284383 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -371,7 +371,6 @@ def revision_show(context, data_dict): def group_show(context, data_dict): '''Shows group details''' - model = context['model'] id = data_dict['id'] api = context.get('api_version') or '1' @@ -910,7 +909,7 @@ def get_site_user(context, data_dict): def roles_show(context, data_dict): '''Returns the roles that users (and authorization groups) have on a particular domain_object. - + If you specify a user (or authorization group) then the resulting roles will be filtered by those of that user (or authorization group). diff --git a/ckan/logic/auth/publisher/get.py b/ckan/logic/auth/publisher/get.py index 7766b1ae1fa..27515958d15 100644 --- a/ckan/logic/auth/publisher/get.py +++ b/ckan/logic/auth/publisher/get.py @@ -65,40 +65,44 @@ def package_show(context, data_dict): """ Package show permission checks the user group if the state is deleted """ model = context['model'] package = get_package_object(context, data_dict) - + if package.state == 'deleted': if 'ignore_auth' in context and context['ignore_auth']: - return {'success': True} - + return {'success': True} + user = context.get('user') if not user: return {'success': False, 'msg': _('User not authorized to read package %s') % (package.id)} userobj = model.User.get( user ) + + if Authorizer().is_sysadmin(unicode(user)): + return {'success': True} + if not userobj: return {'success': False, 'msg': _('User %s not authorized to read package %s') % (str(user),package.id)} if not _groups_intersect( userobj.get_groups('publisher'), package.get_groups('publisher') ): return {'success': False, 'msg': _('User %s not authorized to read package %s') % (str(user),package.id)} - + return {'success': True} def resource_show(context, data_dict): - """ Resource show permission checks the user group if the package state is deleted """ + """ Resource show permission checks the user group if the package state is deleted """ model = context['model'] user = context.get('user') resource = get_resource_object(context, data_dict) - package = resource.revision_group.package + package = resource.resource_group.package if package.state == 'deleted': userobj = model.User.get( user ) if not userobj: - return {'success': False, 'msg': _('User %s not authorized to read resource %s') % (str(user),package.id)} + return {'success': False, 'msg': _('User %s not authorized to read resource %s') % (str(user),package.id)} if not _groups_intersect( userobj.get_groups('publisher'), package.get_groups('publisher') ): return {'success': False, 'msg': _('User %s not authorized to read package %s') % (str(user),package.id)} - - pkg_dict = {'id': pkg.id} + + pkg_dict = {'id': package.id} return package_show(context, pkg_dict) @@ -112,12 +116,12 @@ def group_show(context, data_dict): user = context.get('user') group = get_group_object(context, data_dict) userobj = model.User.get( user ) - + if group.state == 'deleted': if not user or \ not _groups_intersect( userobj.get_groups('publisher'), group.get_groups('publisher') ): - return {'success': False, 'msg': _('User %s not authorized to show group %s') % (str(user),group.id)} - + return {'success': False, 'msg': _('User %s not authorized to show group %s') % (str(user),group.id)} + return {'success': True} def tag_show(context, data_dict): diff --git a/ckan/logic/schema.py b/ckan/logic/schema.py index 164343a4eac..aa0d84ea0c4 100644 --- a/ckan/logic/schema.py +++ b/ckan/logic/schema.py @@ -186,6 +186,13 @@ def default_group_schema(): '__extras': [ignore], 'packages': { "id": [not_empty, unicode, package_id_or_name_exists], + "title":[ignore_missing, unicode], + "name":[ignore_missing, unicode], + "__extras": [ignore] + }, + 'groups': { + "name": [not_empty, unicode], + "capacity": [ignore_missing], "__extras": [ignore] }, 'users': { @@ -206,13 +213,14 @@ def group_form_schema(): #schema['extras_validation'] = [duplicate_extras_key, ignore] schema['packages'] = { "name": [not_empty, unicode], + "title": [ignore_missing], "__extras": [ignore] } schema['users'] = { "name": [not_empty, unicode], - "capacity": [ignore_missing], + "capacity": [ignore_missing], "__extras": [ignore] - } + } return schema diff --git a/ckan/logic/validators.py b/ckan/logic/validators.py index ec635c296cd..290a3c9600f 100644 --- a/ckan/logic/validators.py +++ b/ckan/logic/validators.py @@ -114,6 +114,16 @@ def group_id_exists(group_id, context): raise Invalid('%s: %s' % (_('Not found'), _('Group'))) return group_id +def group_id_or_name_exists(reference, context): + """ + Raises Invalid if a group identified by the name or id cannot be found. + """ + model = context['model'] + result = model.Group.get(reference) + if not result: + raise Invalid(_('That group name or ID does not exist.')) + return reference + def activity_type_exists(activity_type): """Raises Invalid if there is no registered activity renderer for the given activity_type. Otherwise returns the given activity_type. diff --git a/ckan/model/group.py b/ckan/model/group.py index e90b35a4c35..70925563811 100644 --- a/ckan/model/group.py +++ b/ckan/model/group.py @@ -103,6 +103,20 @@ def get(cls, reference): return group # Todo: Make sure group names can't be changed to look like group IDs? + @classmethod + def all(cls, group_type=None, state=('active',)): + """ + Returns all groups. + """ + q = Session.query(cls) + if state: + q = q.filter(cls.state.in_(state)) + + if group_type: + q = q.filter(cls.type==group_type) + + return q.order_by(cls.title) + def set_approval_status(self, status): """ Aproval status can be set on a group, where currently it does @@ -116,17 +130,21 @@ def set_approval_status(self, status): pass def members_of_type(self, object_type, capacity=None): + from ckan import model object_type_string = object_type.__name__.lower() query = Session.query(object_type).\ - filter(group_table.c.id == self.id).\ - filter(member_table.c.state == 'active').\ - filter(member_table.c.table_name == object_type_string) + filter(model.Group.id == self.id).\ + filter(model.Member.state == 'active').\ + filter(model.Member.table_name == object_type_string) + + if hasattr(object_type,'state'): + query = query.filter(object_type.state == 'active' ) if capacity: - query = query.filter(member_table.c.capacity == capacity) + query = query.filter(model.Member.capacity == capacity) - query = query.join(member_table, member_table.c.table_id == getattr(object_type,'id') ).\ - join(group_table, group_table.c.id == member_table.c.group_id) + query = query.join(model.Member, member_table.c.table_id == getattr(object_type,'id') ).\ + join(model.Group, group_table.c.id == member_table.c.group_id) return query @@ -136,6 +154,15 @@ def add_child(self, object_instance): member = Member(group=self, table_id=getattr(object_instance,'id'), table_name=object_type_string) Session.add(member) + def get_children_groups(self, type='group'): + # Returns a list of dicts where each dict contains "id", "name", and "title" + # When querying with a CTE specifying a model in the query parameter causes + # problems as it returns only the first level deep apparently not recursing + # any deeper than that. If we simplify and request only specific fields then + # if returns the full depth of the hierarchy. + results = Session.query("id","name", "title").\ + from_statement(HIERARCHY_CTE).params(id=self.id, type=type).all() + return [ { "id":idf, "name": name, "title": title } for idf,name,title in results ] def active_packages(self, load_eager=True): query = Session.query(Package).\ @@ -242,3 +269,15 @@ def __repr__(self): #TODO MemberRevision.related_packages = lambda self: [self.continuity.package] +HIERARCHY_CTE = """ + WITH RECURSIVE subtree(id) AS ( + SELECT M.* FROM public.member AS M + WHERE M.table_name = 'group' AND M.state = 'active' + UNION + SELECT M.* FROM public.member M, subtree SG + WHERE M.table_id = SG.group_id AND M.table_name = 'group' AND M.state = 'active') + + SELECT G.* FROM subtree AS ST + INNER JOIN public.group G ON G.id = ST.table_id + WHERE group_id = :id AND G.type = :type and table_name='group' +""" diff --git a/ckan/model/user.py b/ckan/model/user.py index ed07c0c5e37..780cb94ef00 100644 --- a/ckan/model/user.py +++ b/ckan/model/user.py @@ -23,15 +23,15 @@ ) class User(DomainObject): - + VALID_NAME = re.compile(r"^[a-zA-Z0-9_\-]{3,255}$") DOUBLE_SLASH = re.compile(':\/([^/])') - + @classmethod def by_openid(cls, openid): obj = Session.query(cls).autoflush(False) return obj.filter_by(openid=openid).first() - + @classmethod def get(cls, user_reference): # double slashes in an openid often get turned into single slashes @@ -57,7 +57,7 @@ def email_hash(self): if self.email: e = self.email.strip().lower().encode('utf8') return hashlib.md5(e).hexdigest() - + def get_reference_preferred_for_uri(self): '''Returns a reference (e.g. name, id, openid) for this user suitable for the user\'s URI. @@ -73,7 +73,7 @@ def get_reference_preferred_for_uri(self): else: ref = self.id return ref - + def _set_password(self, password): """Hash password on the fly.""" if isinstance(password, unicode): @@ -104,7 +104,7 @@ def validate_password(self, password): :return: Whether the password is valid. :rtype: bool """ - if not password or not self.password: + if not password or not self.password: return False if isinstance(password, unicode): password_8bit = password.encode('ascii', 'ignore') @@ -114,7 +114,7 @@ def validate_password(self, password): return self.password[40:] == hashed_pass.hexdigest() password = property(_get_password, _set_password) - + @classmethod def check_name_valid(cls, name): if not name \ @@ -147,10 +147,23 @@ def number_administered_packages(self): def is_in_group(self, group): return group in self.get_groups() - + + def is_in_groups(self, groupids): + """ Given a list of group ids, returns True if this user is in any of + those groups """ + guser = set( self.get_group_ids() ) + gids = set( groupids ) + + return len( guser.intersection( gids ) ) > 0 + + + def get_group_ids(self, group_type=None): + """ Returns a list of group ids that the current user belongs to """ + return [ g.id for g in self.get_groups( group_type=group_type ) ] + def get_groups(self, group_type=None, capacity=None): import ckan.model as model - + q = model.Session.query(model.Group)\ .join(model.Member, model.Member.group_id == model.Group.id and \ model.Member.table_name == 'user' ).\ @@ -160,12 +173,12 @@ def get_groups(self, group_type=None, capacity=None): if capacity: q = q.filter( model.Member.capacity == capacity ) return q.all() - + if '_groups' not in self.__dict__: self._groups = q.all() - + groups = self._groups - if group_type: + if group_type: groups = [g for g in groups if g.type == group_type] return groups @@ -173,7 +186,7 @@ def get_groups(self, group_type=None, capacity=None): @classmethod def search(cls, querystr, sqlalchemy_query=None): '''Search name, fullname, email and openid. - + ''' import ckan.model as model if sqlalchemy_query is None: diff --git a/ckan/public/css/forms.css b/ckan/public/css/forms.css index 97406579d9f..e1a6a46de91 100644 --- a/ckan/public/css/forms.css +++ b/ckan/public/css/forms.css @@ -139,6 +139,12 @@ form.has-errors .field_error, form.has-errors .error-explanation { position: relative; background: transparent url(../images/icons/error.png) left 3px no-repeat; } +td.field_warning { + color: #d12f19; } + +.fieldset_button_error { + background: transparent url(../images/icons/error.png) left center no-repeat; } + .error-explanation, #errorExplanation { background: #fff; diff --git a/ckan/public/css/style.css b/ckan/public/css/style.css index 0143eb36e1a..b8767070e90 100644 --- a/ckan/public/css/style.css +++ b/ckan/public/css/style.css @@ -973,6 +973,7 @@ ul.dataset-edit-nav li a { display: block; padding: 7px 0 7px 10px; margin-bottom: 7px; + margin-left: 20px; border: 1px transparent solid; } ul.dataset-edit-nav li a.active, diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js index e8e40859cfe..dbba9a64861 100644 --- a/ckan/public/scripts/application.js +++ b/ckan/public/scripts/application.js @@ -94,6 +94,15 @@ $(e.target).attr('disabled','disabled'); return false; }); + + // Highlight form errors in the tab buttons + for (field_id in form_errors) { + var field = $('#'+field_id); + if (field !== undefined) { + var fieldset_id = field.parents('fieldset').last().attr('id'); + $('#section-'+fieldset_id).addClass('fieldset_button_error'); + } + } } var isGroupEdit = $('body.group.edit').length > 0; if (isGroupEdit) { @@ -395,10 +404,9 @@ CKAN.Utils = function($, my) { input_box.attr('name', new_name) input_box.attr('id', new_name) - var capacity = $("input:radio[name=add-user-capacity]:checked").val(); parent_dd.before( '' + - '' + + '' + '
' + ui.item.label + '
' ); @@ -825,8 +833,37 @@ CKAN.View.ResourceAddLink = Backbone.View.extend({ setResourceInfo: function(e) { e.preventDefault(); + this.el.find('input[name=save]').addClass("disabled"); + this.el.find('input[name=reset]').addClass("disabled"); var urlVal=this.el.find('input[name=url]').val(); - this.model.set({url: urlVal, resource_type: this.mode}) + var qaEnabled = $.inArray('qa',CKAN.plugins)>=0; + + if(qaEnabled && this.mode=='file') { + $.ajax({ + url: CKAN.SITE_URL + '/qa/link_checker', + context: this.model, + data: {url: urlVal}, + dataType: 'json', + error: function(){ + this.set({url: urlVal, resource_type: 'file'}); + }, + success: function(data){ + data = data[0]; + this.set({ + url: urlVal, + resource_type: 'file', + format: data.format, + size: data.size, + mimetype: data.mimetype, + last_modified: data.last_modified, + url_error: (data.url_errors || [""])[0] + }); + } + }); + } else { + this.model.set({url: urlVal, resource_type: this.mode}); + } + } }); diff --git a/ckan/public/scripts/templates.js b/ckan/public/scripts/templates.js index a4c33ce93a5..924136ab4f7 100644 --- a/ckan/public/scripts/templates.js +++ b/ckan/public/scripts/templates.js @@ -83,14 +83,14 @@ CKAN.Templates.resourceEntry = ' \ \ \ \ - '+CKAN.Strings.url+' \ + '+CKAN.Strings.url+' \ \ {{if resource.resource_type=="file.upload"}} \ ${resource.url} \ \ {{/if}} \ {{if resource.resource_type!="file.upload"}} \ - \ + \ {{/if}} \ \ \ diff --git a/ckan/templates/facets.html b/ckan/templates/facets.html index 0ffe47bbd91..7cafdcaea3b 100644 --- a/ckan/templates/facets.html +++ b/ckan/templates/facets.html @@ -5,18 +5,36 @@ py:strip="" > - -
+ +

${title(code)}

+

${if_empty}

+ + +
  • ${if_empty}
  • +
  • + ${label(name)} (${count}) +
  • +
    +
    diff --git a/ckan/templates/package/edit.html b/ckan/templates/package/edit.html index 42e60447394..d2543cc350a 100644 --- a/ckan/templates/package/edit.html +++ b/ckan/templates/package/edit.html @@ -9,6 +9,7 @@ @@ -16,11 +17,11 @@
  • diff --git a/ckan/tests/functional/api/model/test_group.py b/ckan/tests/functional/api/model/test_group.py index 6a4b1e7c250..f7e5a62b16b 100644 --- a/ckan/tests/functional/api/model/test_group.py +++ b/ckan/tests/functional/api/model/test_group.py @@ -3,12 +3,12 @@ from ckan import model from ckan.lib.create_test_data import CreateTestData -from nose.tools import assert_equal +from nose.tools import assert_equal from ckan.tests.functional.api.base import BaseModelApiTestCase -from ckan.tests.functional.api.base import Api1TestCase as Version1TestCase -from ckan.tests.functional.api.base import Api2TestCase as Version2TestCase -from ckan.tests.functional.api.base import ApiUnversionedTestCase as UnversionedTestCase +from ckan.tests.functional.api.base import Api1TestCase as Version1TestCase +from ckan.tests.functional.api.base import Api2TestCase as Version2TestCase +from ckan.tests.functional.api.base import ApiUnversionedTestCase as UnversionedTestCase class GroupsTestCase(BaseModelApiTestCase): @@ -17,7 +17,7 @@ def setup_class(cls): CreateTestData.create() cls.user_name = u'russianfan' # created in CreateTestData cls.init_extra_environ(cls.user_name) - + @classmethod def teardown_class(cls): model.repo.rebuild_db() @@ -73,11 +73,11 @@ def test_register_post_ok(self): status=self.STATUS_409_CONFLICT, extra_environ=self.extra_environ) self.assert_json_response(res, 'Group name already exists') - + def test_entity_get_ok(self): offset = self.group_offset(self.roger.name) res = self.app.get(offset, status=self.STATUS_200_OK) - + self.assert_msg_represents_roger(msg=res.body) assert self.package_ref_from_name('annakarenina') in res, res assert self.group_ref_from_name('roger') in res, res @@ -146,7 +146,7 @@ def test_10_edit_group_name_duplicate(self): rev = model.repo.new_revision() model.repo.commit_and_remove() assert model.Group.by_name(self.testgroupvalues['name']) - + # create a group with name 'dupname' dupname = u'dupname' if not model.Group.by_name(dupname): @@ -164,7 +164,7 @@ def test_10_edit_group_name_duplicate(self): res = self.app.post(offset, params=postparams, status=[409], extra_environ=self.extra_environ) self.assert_json_response(res, 'Group name already exists') - + def test_11_delete_group(self): # Test Groups Entity Delete 200. diff --git a/ckan/tests/functional/test_authz.py b/ckan/tests/functional/test_authz.py index 436f9cffa10..aa206242931 100644 --- a/ckan/tests/functional/test_authz.py +++ b/ckan/tests/functional/test_authz.py @@ -19,7 +19,7 @@ class AuthzTestBase(object): ENTITY_CLASS_MAP = {'dataset': model.Package, 'group': model.Group, 'package_relationship': model.PackageRelationship} - + @classmethod def setup_class(self): setup_test_search_index() @@ -111,10 +111,14 @@ def _test_via_wui(self, action, user, entity_name, entity='dataset'): str_required_in_response = '