diff --git a/ckan/config/routing.py b/ckan/config/routing.py index a50181bbe1e..212a42dc6b2 100644 --- a/ckan/config/routing.py +++ b/ckan/config/routing.py @@ -57,6 +57,7 @@ def make_map(): 'resource', 'tag', 'group', + 'organization', 'related', 'revision', 'licenses', @@ -119,6 +120,7 @@ def make_map(): m.connect('/util/resource/format_icon', action='format_icon', conditions=GET) m.connect('/util/group/autocomplete', action='group_autocomplete') + m.connect('/util/organization/autocomplete', action='organization_autocomplete') m.connect('/util/markdown', action='markdown') m.connect('/util/dataset/munge_name', action='munge_package_name') m.connect('/util/dataset/munge_title_to_name', @@ -218,9 +220,8 @@ def make_map(): map.redirect('/groups', '/group') map.redirect('/groups/{url:.*}', '/group/{url}') - ##to get back formalchemy uncomment these lines - ##map.connect('/group/new', controller='group_formalchemy', action='new') - ##map.connect('/group/edit/{id}', controller='group_formalchemy', action='edit') + map.redirect('/organizations', '/organization') + map.redirect('/organizations/{url:.*}', '/organization/{url}') # These named routes are used for custom group forms which will use the # names below based on the group.type ('group' is the default type) @@ -238,6 +239,26 @@ def make_map(): ) m.connect('group_read', '/group/{id}', action='read') + # These are the routes for organizations + with SubMapper(map, controller='organization') as m: + m.connect('organization_index', '/organization', action='index') + m.connect('organization_list', '/organization/list', action='list') + m.connect('organization_new', '/organization/new', action='new') + m.connect('organization_users', '/organization/users/{id}', + action='users') + m.connect('organization_apply_named', '/organization/apply/{id}', + action='apply') + m.connect('organization_apply', '/organization/apply', + action='apply') + m.connect('organization_action', '/organization/{action}/{id}', + requirements=dict(action='|'.join([ + 'edit', + 'history' + ])) + ) + m.connect('organization_read', '/organization/{id}', action='read') + + register_package_plugins(map) register_group_plugins(map) diff --git a/ckan/controllers/api.py b/ckan/controllers/api.py index b05721a796d..edb8e1666d8 100644 --- a/ckan/controllers/api.py +++ b/ckan/controllers/api.py @@ -638,6 +638,29 @@ def convert_to_dict(user): out = map(convert_to_dict, query.all()) return out + @jsonp.jsonpify + def organization_autocomplete(self): + q = request.params.get('q', '') + limit = request.params.get('limit', 20) + try: + limit = int(limit) + except: + limit = 20 + limit = min(50, limit) + + query = model.Group.search_by_name_or_title(q, 'organization') + + def convert_to_dict(user): + out = {} + for k in ['id', 'name', 'title']: + out[k] = getattr(user, k) + return out + + query = query.limit(limit) + out = map(convert_to_dict, query.all()) + return out + + def is_slug_valid(self): slug = request.params.get('slug') or '' slugtype = request.params.get('type') or '' diff --git a/ckan/controllers/organization.py b/ckan/controllers/organization.py new file mode 100644 index 00000000000..dd55598c6cb --- /dev/null +++ b/ckan/controllers/organization.py @@ -0,0 +1,566 @@ +import logging +import genshi +import datetime +from urllib import urlencode + +from ckan.lib.base import (BaseController, c, model, request, render, h, g) +import ckan.lib.dictization.model_save as model_save + +from ckan.lib.base import abort +import pylons.config as config +from pylons.i18n import get_lang, _ +from ckan.lib.helpers import Page +import ckan.lib.maintain as maintain +from ckan.lib.navl.dictization_functions import (DataError, unflatten) +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 +import ckan.logic.schema as schema +import ckan.authz as authz +import ckan.forms +import ckan.logic as logic +import ckan.logic.action.get +import ckan.logic.action as action +import ckan.lib.search as search +import ckan.lib.mailer as mailer + + +log = logging.getLogger(__name__) + + +class OrganizationController(BaseController): + """ + An Organization is modelled in the same way as a group, it is + implemented using the Group model and uses the type field to + differentiate itself from normal 'Groups'. We provide a different + controller and templates to make it easy to differentiate between the + two and so they can diverge. + """ + + def form_to_db_schema(self): + return schema.group_form_schema() + + def db_to_form_schema(self): + '''This is an interface to manipulate data from the database + into a format suitable for the form (optional)''' + + def index(self): + context = {'model': model, 'session': model.Session, + 'user': c.user or c.author, 'for_view': True, + 'with_private': False} + + data_dict = {'all_fields': True} + + try: + check_access('site_read', context) + except NotAuthorized: + abort(401, _('Not authorized to see this page')) + + results = get_action('organization_list')(context, data_dict) + + c.page = Page( + collection=results, + page=request.params.get('page', 1), + url=h.pager_url, + items_per_page=20 + ) + return render('organization/index.html') + + def read(self, id): + from ckan.lib.search import SearchError + context = {'model': model, 'session': model.Session, + 'user': c.user or c.author, + 'schema': self.db_to_form_schema(), + 'for_view': True, 'extras_as_string': True} + data_dict = {'id': id} + # unicode format (decoded from utf8) + q = c.q = request.params.get('q', '') + + try: + c.organization_dict = get_action('organization_show')(context, data_dict) + c.organization = context['organization'] + except NotFound: + abort(404, _('Organization not found')) + except NotAuthorized: + abort(401, _('Unauthorized to read organization %s') % id) + + # Search within group + q += ' groups: "%s"' % c.organization_dict.get('name') + + try: + description_formatted = ckan.misc.MarkdownFormat().to_html( + c.organization_dict.get('description', '')) + c.description_formatted = genshi.HTML(description_formatted) + except Exception: + error_msg = "%s" %\ + _("Cannot render description") + c.description_formatted = genshi.HTML(error_msg) + + + c.organization_admins = c.organization.members_of_type(model.User, 'admin').all() + c.organization_members = c.organization.members_of_type(model.User, 'editor').all() + + context['return_query'] = True + + limit = 20 + try: + page = int(request.params.get('page', 1)) + except ValueError: + abort(400, ('"page" parameter must be an integer')) + + # most search operations should reset the page counter: + params_nopage = [(k, v) for k, v in request.params.items() + if k != 'page'] + sort_by = request.params.get('sort', None) + + def search_url(params): + url = h.url_for(controller='organization', action='read', + id=c.organization_dict.get('name')) + params = [(k, v.encode('utf-8') if isinstance(v, basestring) + else str(v)) for k, v in params] + return url + u'?' + urlencode(params) + + def drill_down_url(**by): + params = list(params_nopage) + params.extend(by.items()) + return search_url(set(params)) + + c.drill_down_url = drill_down_url + + def remove_field(key, value): + params = list(params_nopage) + params.remove((key, value)) + return search_url(params) + + c.remove_field = remove_field + + def pager_url(q=None, page=None): + params = list(params_nopage) + params.append(('page', page)) + return search_url(params) + + try: + c.fields = [] + search_extras = {} + for (param, value) in request.params.items(): + if not param in ['q', 'page', 'sort'] \ + and len(value) and not param.startswith('_'): + if not param.startswith('ext_'): + c.fields.append((param, value)) + q += ' %s: "%s"' % (param, value) + else: + search_extras[param] = value + + fq = 'capacity:"public"' + if (c.userobj and c.organization and c.userobj.is_in_group(c.organization)): + fq = '' + context['ignore_capacity_check'] = True + + data_dict = { + 'q': q, + 'fq': fq, + 'facet.field': g.facets, + 'rows': limit, + 'sort': sort_by, + 'start': (page - 1) * limit, + 'extras': search_extras + } + + query = get_action('package_search')(context, data_dict) + + c.page = h.Page( + collection=query['results'], + page=page, + url=pager_url, + item_count=query['count'], + items_per_page=limit + ) + + c.facets = query['facets'] + maintain.deprecate_context_item( + 'facets', + 'Use `c.search_facets` instead.') + + c.search_facets = query['search_facets'] + c.page.items = query['results'] + + c.sort_by_selected = sort_by + + except SearchError, se: + log.error('Organization search error: %r', se.args) + c.query_error = True + c.facets = {} + c.page = h.Page(collection=[]) + + # Add the organizations's activity stream (already rendered to HTML) + # to the template context for the group/read.html template to retrieve + # later. + c.organization_activity_stream = \ + get_action('organization_activity_list_html')(context, + {'id': c.organization_dict['id']}) + + return render('organization/read.html') + + def new(self, data=None, errors=None, error_summary=None): + if data: + data['type'] = 'organization' + + context = {'model': model, 'session': model.Session, + 'user': c.user or c.author, 'extras_as_string': True, + 'save': 'save' in request.params, + 'parent': request.params.get('parent', None)} + try: + check_access('organization_create', context) + except NotAuthorized: + abort(401, _('Unauthorized to create an organization')) + + if context['save'] and not data: + return self._save_new(context) + + data = data or {} + errors = errors or {} + error_summary = error_summary or {} + vars = {'data': data, 'errors': errors, 'error_summary': error_summary} + + self.setup_template_variables(context, data) + c.form = render('organization/organization_form.html', + extra_vars=vars) + return render('organization/new.html') + + def edit(self, id, data=None, errors=None, error_summary=None): + context = {'model': model, 'session': model.Session, + 'user': c.user or c.author, 'extras_as_string': True, + 'save': 'save' in request.params, + 'for_edit': True, + 'parent': request.params.get('parent', None) + } + data_dict = {'id': id} + + if context['save'] and not data: + return self._save_edit(id, context) + + try: + old_data = get_action('organization_show')(context, data_dict) + c.organizationtitle = old_data.get('title') + c.organizationname = old_data.get('name') + data = data or old_data + except NotFound: + abort(404, _('Organization not found')) + except NotAuthorized: + abort(401, _('Unauthorized to read organization %s') % '') + + organization = context.get("organization") + c.organization = organization + + try: + check_access('organization_update', context) + except NotAuthorized: + abort(401, _('User %r not authorized to edit %s') % (c.user, id)) + + errors = errors or {} + vars = {'data': data, 'errors': errors, 'error_summary': error_summary} + + self.setup_template_variables(context, data) + c.form = render('organization/organization_form.html', extra_vars=vars) + return render('organization/edit.html') + + + def _save_new(self, context): + try: + data_dict = clean_dict(unflatten( + tuplize_dict(parse_params(request.params)))) + data_dict['type'] = 'organization' + context['message'] = data_dict.get('log_message', '') + data_dict['users'] = [{'name': c.user, 'capacity': 'admin'}] + group = get_action('organization_create')(context, data_dict) + + # Redirect to the appropriate _read route for the type of group + h.redirect_to(group['type'] + '_read', id=group['name']) + except NotAuthorized: + abort(401, _('Unauthorized to read organization %s') % '') + except NotFound, e: + abort(404, _('Organization not found')) + except DataError: + abort(400, _(u'Integrity Error')) + except ValidationError, e: + errors = e.error_dict + error_summary = e.error_summary + return self.new(data_dict, errors, error_summary) + + def _force_reindex(self, grp): + ''' When the group name has changed, we need to force a reindex + of the datasets within the group, otherwise they will stop + appearing on the read page for the group (as they're connected via + the group name)''' + group = model.Group.get(grp['name']) + for dataset in group.active_packages().all(): + search.rebuild(dataset.name) + + def _save_edit(self, id, context): + try: + data_dict = clean_dict(unflatten( + tuplize_dict(parse_params(request.params)))) + context['message'] = data_dict.get('log_message', '') + data_dict['id'] = id + context['allow_partial_update'] = True + organization = get_action('organization_update')(context, data_dict) + + if id != organization['name']: + self._force_reindex(organization) + + h.redirect_to('organization_read', id=organization['name']) + except NotAuthorized: + abort(401, _('Unauthorized to read organization %s') % id) + except NotFound, e: + abort(404, _('Organization not found')) + except DataError: + abort(400, _(u'Integrity Error')) + except ValidationError, e: + errors = e.error_dict + error_summary = e.error_summary + return self.edit(id, data_dict, errors, error_summary) + + def history(self, id): + if 'diff' in request.params or 'selected1' in request.params: + try: + params = {'id': request.params.getone('group_name'), + 'diff': request.params.getone('selected1'), + 'oldid': request.params.getone('selected2'), + } + except KeyError: + if 'group_name' in dict(request.params): + id = request.params.getone('group_name') + c.error = \ + _('Select two revisions before doing the comparison.') + else: + params['diff_entity'] = 'group' + h.redirect_to(controller='revision', action='diff', **params) + + context = {'model': model, 'session': model.Session, + 'user': c.user or c.author, + 'schema': self.form_to_db_schema()} + data_dict = {'id': id} + try: + c.organization_dict = get_action('organization_show')(context, data_dict) + c.organization_revisions = get_action('organization_revision_list')(context, + data_dict) + c.organization = context['organization'] + except NotFound: + abort(404, _('Organization not found')) + except NotAuthorized: + abort(401, _('User %r not authorized to edit %r') % (c.user, id)) + + format = request.params.get('format', '') + if format == 'atom': + # Generate and return Atom 1.0 document. + from webhelpers.feedgenerator import Atom1Feed + feed = Atom1Feed( + title=_(u'CKAN Organization Revision History'), + link=h.url_for(controller='organization', action='read', + id=c.organization_dict['name']), + description=_(u'Recent changes to CKAN Organization: ') + + c.organization_dict['display_name'], + language=unicode(get_lang()), + ) + for revision_dict in c.organization_revisions: + revision_date = h.date_str_to_datetime( + revision_dict['timestamp']) + try: + dayHorizon = int(request.params.get('days')) + except: + dayHorizon = 30 + dayAge = (datetime.datetime.now() - revision_date).days + if dayAge >= dayHorizon: + break + if revision_dict['message']: + item_title = u'%s' % revision_dict['message'].\ + split('\n')[0] + else: + item_title = u'%s' % revision_dict['id'] + item_link = h.url_for(controller='revision', action='read', + id=revision_dict['id']) + item_description = _('Log message: ') + item_description += '%s' % (revision_dict['message'] or '') + item_author_name = revision_dict['author'] + item_pubdate = revision_date + feed.add_item( + title=item_title, + link=item_link, + description=item_description, + author_name=item_author_name, + pubdate=item_pubdate, + ) + feed.content_type = 'application/atom+xml' + return feed.writeString('utf-8') + return render('organization/history.html') + + def _render_edit_form(self, fs): + # errors arrive in c.error and fs.errors + c.fieldset = fs + c.fieldset2 = ckan.forms.get_package_group_fieldset() + return render('group/edit_form.html') + + def _send_application(self, organization, reason): + from genshi.template.text import NewTextTemplate + + if not reason: + h.flash_error(_("There was a problem with your submission, \ + please correct it and try again")) + errors = {"reason": ["No reason was supplied"]} + return self.apply(organization.id, errors=errors, + error_summary=action.error_summary(errors)) + + admins = organization.members_of_type(model.User, 'admin').all() + recipients = [(u.fullname, u.email) for u in admins] if admins else \ + [(config.get('ckan.admin.name', "CKAN Administrator"), + config.get('ckan.admin.email', None), )] + + if not recipients: + h.flash_error( + _("There is a problem with the system configuration")) + errors = {"reason": ["No group administrator exists"]} + return self.apply(group.id, data=None, errors=errors, + error_summary=action.error_summary(errors)) + + extra_vars = { + 'group': group, + 'requester': c.userobj, + 'reason': reason + } + email_msg = render("organization/email/join_publisher_request.txt", + extra_vars=extra_vars, + loader_class=NewTextTemplate) + + try: + for (name, recipient) in recipients: + mailer.mail_recipient(name, + recipient, + "Organization request", + email_msg) + except: + h.flash_error( + _("There is a problem with the system configuration")) + errors = {"reason": ["No mail server was found"]} + return self.apply(group.id, errors=errors, + error_summary=action.error_summary(errors)) + + h.flash_success(_("Your application has been submitted")) + h.redirect_to('organization_read', id=group.name) + + def apply(self, id=None, data=None, errors=None, error_summary=None): + """ + A user has requested access to this publisher and so we will send an + email to any admins within the publisher. + """ + if 'parent' in request.params and not id: + id = request.params['parent'] + + if id: + c.organization = model.Group.get(id) + if 'save' in request.params and not errors: + return self._send_application( + c.organization, request.params.get('reason', None)) + + self._add_organization_list() + data = data or {} + errors = errors or {} + error_summary = error_summary or {} + + data.update(request.params) + + vars = {'data': data, 'errors': errors, 'error_summary': error_summary} + c.form = render('organization/apply_form.html', extra_vars=vars) + return render('organization/apply.html') + + def _add_users(self, group, parameters): + if not group: + h.flash_error(_("There was a problem with your submission, \ + please correct it and try again")) + errors = {"reason": ["No reason was supplied"]} + return self.apply(group.id, errors=errors, + error_summary=action.error_summary(errors)) + + data_dict = logic.clean_dict(unflatten( + logic.tuplize_dict(logic.parse_params(request.params)))) + data_dict['id'] = group.id + + # Temporary fix for strange caching during dev + l = data_dict['users'] + for d in l: + d['capacity'] = d.get('capacity', 'editor') + + context = { + "group": group, + "schema": schema.default_group_schema(), + "model": model, + "session": model.Session + } + + # Temporary cleanup of a capacity being sent without a name + users = [d for d in data_dict['users'] if len(d) == 2] + data_dict['users'] = users + + model.repo.new_revision() + model_save.group_member_save(context, data_dict, 'users') + model.Session.commit() + + h.redirect_to(controller='organization', action='edit', id=group.name) + + def users(self, id, data=None, errors=None, error_summary=None): + c.organization = model.Group.get(id) + + if not c.organization: + abort(404, _('Group not found')) + + context = { + 'model': model, + 'session': model.Session, + 'user': c.user or c.author, + 'organization': c.organization} + + try: + logic.check_access('organization_update', context) + except logic.NotAuthorized: + abort(401, _('User %r not authorized to edit %s') % (c.user, id)) + + if 'save' in request.params and not errors: + return self._add_users(c.organization, request.params) + + data = data or {} + errors = errors or {} + error_summary = error_summary or {} + + data['users'] = [] + data['users'].extend({"name": user.name, + "capacity": "admin"} + for user in c.organization.members_of_type( + model.User, "admin").all()) + data['users'].extend({"name": user.name, + "capacity": "editor"} + for user in c.organization.members_of_type( + model.User, 'editor').all()) + + vars = {'data': data, 'errors': errors, 'error_summary': error_summary} + c.form = render('organization/users_form.html', extra_vars=vars) + + return render('organization/users.html') + + def _add_organization_list(self): + c.possible_parents = model.Session.query(model.Group).\ + filter(model.Group.state == 'active').\ + filter(model.Group.type == 'organization').\ + order_by(model.Group.title).all() + + def setup_template_variables(self, context, data_dict): + c.is_sysadmin = authz.Authorizer().is_sysadmin(c.user) + + context_organization = context.get('organization', None) + organization = context_organization or c.organization + if organization: + try: + if not context_organization: + context['organization'] = organization + logic.check_access('organization_change_state', context) + c.auth_for_change_state = True + except logic.NotAuthorized: + c.auth_for_change_state = False diff --git a/ckan/lib/dictization/model_dictize.py b/ckan/lib/dictization/model_dictize.py index 97869588391..2f472d82114 100644 --- a/ckan/lib/dictization/model_dictize.py +++ b/ckan/lib/dictization/model_dictize.py @@ -42,6 +42,40 @@ def group_list_dictize(obj_list, context, result_list.append(group_dict) return sorted(result_list, key=sort_key, reverse=reverse) +def organizations_list_dictize(obj_list, context, + sort_key=lambda x:x['display_name'], + reverse=False): + + active = context.get('active', True) + with_private = context.get('include_private_packages', False) + result_list = [] + + for obj in obj_list: + if context.get('with_capacity'): + obj, capacity = obj + organization_dict = d.table_dictize(obj, context, capacity=capacity) + else: + organization_dict = d.table_dictize(obj, context) + organization_dict.pop('created') + if active and obj.state not in ('active', 'pending'): + continue + + organization_dict['display_name'] = obj.display_name + + organization_dict['packages'] = \ + len(obj.active_packages(with_private=with_private).all()) + organization_dict['users'] = \ + obj.members_of_type(ckan.model.User).count() + + if context.get('for_view'): + for item in plugins.PluginImplementations( + plugins.IGroupController): + organization_dict = item.before_view(organization_dict) + + result_list.append(organization_dict) + return sorted(result_list, key=sort_key, reverse=reverse) + + def resource_list_dictize(res_list, context): active = context.get('active', True) diff --git a/ckan/logic/action/create.py b/ckan/logic/action/create.py index 05068ac95ba..e2cc21fcab6 100644 --- a/ckan/logic/action/create.py +++ b/ckan/logic/action/create.py @@ -588,6 +588,110 @@ def group_create(context, data_dict): log.debug('Created object %s' % str(group.name)) return model_dictize.group_dictize(group, context) +def organization_create(context, data_dict): + '''Create a new organization. + + You must be authorized to create organizations. + + :param name: the name of the group, a string between 2 and 100 characters + long, containing only lowercase alphanumeric characters, ``-`` and + ``_`` + :type name: string + :param id: the id of the organization (optional) + :type id: string + :param title: the title of the organization (optional) + :type title: string + :param description: the description of the group (optional) + :type description: string + :param image_url: the URL to an image to be displayed on the organizations's page + (optional) + :type image_url: string + :param state: the current state of the organization, e.g. ``'active'`` or + ``'deleted'``, only active organizations show up in search results and + other lists of organizations, this parameter will be ignored if you are not + authorized to change the state of the organization (optional, default: + ``'active'``) + :type state: string + :param approval_status: (optional) + :type approval_status: string + :param extras: the organization's extras (optional), extras are arbitrary + (key: value) metadata items that can be added to groups, each extra + dictionary should have keys ``'key'`` (a string), ``'value'`` (a + string), and optionally ``'deleted'`` + :type extras: list of dataset extra dictionaries + :param packages: the datasets (packages) that belong to the organization, a list + of dictionaries each with keys ``'name'`` (string, the id or name of + the dataset) and optionally ``'title'`` (string, the title of the + dataset) + :type packages: list of dictionaries + :param organization: the organization that belong to the organization, + a list of dictionaries each with key ``'name'`` (string, the id or + name of the group) and optionally ``'capacity'`` (string, the capacity + in which the group is a member of the group) + :type organizations: list of dictionaries + :param users: the users that belong to the organization, a list of dictionaries + each with key ``'name'`` (string, the id or name of the user) and + optionally ``'capacity'`` (string, the capacity in which the user is + a member of the organization) + :type users: list of dictionaries + + :returns: the newly created organization + :rtype: dictionary + + ''' + model = context['model'] + user = context['user'] + session = context['session'] + parent = context.get('parent', None) + + _check_access('organization_create', context, data_dict) + + schema = ckan.logic.schema.default_group_schema() + + data, errors = _validate(data_dict, schema, context) + log.debug('group_create validate_errs=%r user=%s group=%s data_dict=%r', + errors, context.get('user'), data_dict.get('name'), data_dict) + + if errors: + session.rollback() + raise ValidationError(errors) + + rev = model.repo.new_revision() + rev.author = user + + if 'message' in context: + rev.message = context['message'] + else: + rev.message = _(u'REST API: Create object %s') % data.get("name") + + organization = model_save.group_dict_save(data, context) + + if parent: + parent_group = model.Group.get( parent ) + if parent_group: + member = model.Member(group=parent_group, table_id=organization.id, table_name='group') + session.add(member) + log.debug('Organization %s is made child of organization %s', + group.name, parent_group.name) + + if user: + admins = [model.User.by_name(user.decode('utf8'))] + else: + admins = [] + + # Needed to let extensions know the group id + session.flush() + + for item in plugins.PluginImplementations(plugins.IGroupController): + item.create(organization) + + if not context.get('defer_commit'): + model.repo.commit() + context["organization"] = organization + context["id"] = organization.id + log.debug(u'Created object %s' % organization.name) + return model_dictize.group_dictize(organization, context) + def rating_create(context, data_dict): '''Rate a dataset (package). @@ -745,6 +849,22 @@ def group_create_rest(context, data_dict): return group_dict +def organization_create_rest(context, data_dict): + + _check_access('organization_create_rest', context, data_dict) + + dictized_organization = model_save.group_api_to_dict(data_dict, context) + dictized_after = _get_action('organization_create')(context, dictized_organization) + + organization = context['organization'] + + organization_dict = model_dictize.group_to_api(group, context) + + data_dict['id'] = organization.id + + return organization_dict + + def vocabulary_create(context, data_dict): '''Create a new tag vocabulary. diff --git a/ckan/logic/action/delete.py b/ckan/logic/action/delete.py index 85f23b0ce5e..f7d5e28f073 100644 --- a/ckan/logic/action/delete.py +++ b/ckan/logic/action/delete.py @@ -219,6 +219,38 @@ def group_delete(context, data_dict): model.repo.commit() +def organization_delete(context, data_dict): + '''Delete an organization. + + You must be authorized to delete the organization. + + :param id: the name or id of the organization + :type id: string + + ''' + model = context['model'] + user = context['user'] + id = _get_or_bust(data_dict, 'id') + + organization = model.Group.get(id) + context['organization'] = organization + if organization is None: + raise NotFound('Organization was not found.') + + revisioned_details = 'Organization: %s' % group.name + + _check_access('organization_delete', context, data_dict) + + rev = model.repo.new_revision() + rev.author = user + rev.message = _(u'REST API: Delete %s') % revisioned_details + organization.delete() + + for item in plugins.PluginImplementations(plugins.IGroupController): + item.delete(organization) + + model.repo.commit() + def task_status_delete(context, data_dict): '''Delete a task status. diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index a7d6dd59e7d..27a97d1d9df 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -342,6 +342,70 @@ def group_list(context, data_dict): return group_list +def organization_list(context, data_dict): + '''Return a list of the names of the site's organizations. + + :param order_by: the field to sort the list by, must be ``'name'`` or + ``'packages'`` (optional, default: ``'name'``) Deprecated use sort. + :type order_by: string + :param sort: sorting of the search results. Optional. Default: + "name asc" string of field name and sort-order. The allowed fields are + 'name' and 'packages' + :type sort: string + :param organizations: a list of names of the organizations to return, if given only + organizations whose names are in this list will be returned (optional) + :type organizations: list of strings + :param all_fields: return full organization dictionaries instead of just names + (optional, default: ``False``) + :type all_fields: boolean + + :rtype: list of strings + + ''' + + model = context['model'] + api = context.get('api_version') + organizations = data_dict.get('groups') + ref_group_by = 'id' if api == 2 else 'name'; + + sort = data_dict.get('sort', 'name') + # order_by deprecated in ckan 1.8 + # if it is supplied and sort isn't use order_by and raise a warning + order_by = data_dict.get('order_by') + if order_by: + log.warn('`order_by` deprecated please use `sort`') + if not data_dict.get('sort'): + sort = order_by + # if the sort is packages and no sort direction is supplied we want to do a + # reverse sort to maintain compatibility. + if sort.strip() == 'packages': + sort = 'packages desc' + + sort_info = _unpick_search(sort, + allowed_fields=['name', 'packages'], + total=1) + + all_fields = data_dict.get('all_fields', None) + + _check_access('organization_list', context, data_dict) + + query = model.Session.query(model.Group).join(model.GroupRevision) + query = query.filter(model.GroupRevision.state=='active') + query = query.filter(model.GroupRevision.current==True) + if organizations: + query = query.filter(model.GroupRevision.name.in_(organizations)) + + organizations = query.all() + organization_list = model_dictize.organizations_list_dictize(organizations, context, + lambda x:x[sort_info[0][0]], + sort_info[0][1] == 'desc') + + if not all_fields: + organization_list = [organization[ref_group_by] for group in organization_list] + + return organization_list + + def group_list_authz(context, data_dict): '''Return the list of groups that the user is authorized to edit. @@ -393,6 +457,30 @@ def group_revision_list(context, data_dict): include_groups=False)) return revision_dicts +def organization_revision_list(context, data_dict): + '''Return a organization's revisions. + + :param id: the name or id of the organization + :type id: string + + :rtype: list of dictionaries + + ''' + model = context['model'] + id = _get_or_bust(data_dict, 'id') + organization = model.Group.get(id) + if organization is None: + raise NotFound + + _check_access('organization_revision_list',context, data_dict) + + revision_dicts = [] + for revision, object_revisions in organization.all_related_revisions: + revision_dicts.append(model.revision_as_dict(revision, + include_packages=False, + include_groups=False)) + return revision_dicts + def licence_list(context, data_dict): '''Return the list of licenses available for datasets on the site. @@ -724,6 +812,34 @@ def group_show(context, data_dict): group_dict, errors = _validate(group_dict, schema, context=context) return group_dict +def organization_show(context, data_dict): + '''Return the details of a organization. + + :param id: the id or name of the organization + :type id: string + + :rtype: dictionary + + ''' + model = context['model'] + id = _get_or_bust(data_dict, 'id') + + organization = model.Group.get(id) + context['organization'] = organization + + if organization is None: + raise NotFound + + _check_access('organization_show',context, data_dict) + + organization_dict = model_dictize.group_dictize(organization, context) + + for item in plugins.PluginImplementations(plugins.IGroupController): + item.read(organization) + + return organization_dict + + def group_package_show(context, data_dict): '''Return the datasets (packages) of a group. @@ -767,6 +883,49 @@ def group_package_show(context, data_dict): return result +def organization_package_show(context, data_dict): + '''Return the datasets (packages) of a organization. + + :param id: the id or name of the organization + :type id: string + :param limit: the maximum number of datasets to return (optional) + :type limit: int + + :rtype: list of dictionaries + + ''' + model = context["model"] + user = context["user"] + id = _get_or_bust(data_dict, 'id') + limit = data_dict.get("limit") + + organization = model.Group.get(id) + context['organization'] = organization + if organization is None: + raise NotFound + + _check_access('organization_show', context, data_dict) + + query = model.Session.query(model.PackageRevision)\ + .filter(model.PackageRevision.state=='active')\ + .filter(model.PackageRevision.current==True)\ + .join(model.Member, model.Member.table_id==model.PackageRevision.id)\ + .join(model.Group, model.Group.id==model.Member.group_id)\ + .filter_by(id=group.id)\ + .order_by(model.PackageRevision.name) + + if limit: + query = query.limit(limit) + + if context.get('return_query'): + return query + + result = [] + for pkg_rev in query.all(): + result.append(model_dictize.package_dictize(pkg_rev, context)) + + return result + def tag_show(context, data_dict): '''Return the details of a tag and all its datasets. @@ -876,6 +1035,14 @@ def group_show_rest(context, data_dict): return group_dict +def organization_show_rest(context, data_dict): + _check_access('organization_show_rest',context, data_dict) + logic.get_action('organization_show')(context, data_dict) + organization = context['organization'] + organization_dict = model_dictize.group_to_api(organization, context) + return organization_dict + + def tag_show_rest(context, data_dict): _check_access('tag_show_rest',context, data_dict) @@ -1754,6 +1921,25 @@ def group_activity_list(context, data_dict): activity_objects = query.all() return model_dictize.activity_list_dictize(activity_objects, context) +def organization_activity_list(context, data_dict): + '''Return a organization's activity stream. + + :param id: the id or name of the organization + :type id: string + + :rtype: list of dictionaries + + ''' + model = context['model'] + organization_id = _get_or_bust(data_dict, 'id') + query = model.Session.query(model.Activity) + query = query.filter_by(object_id=organization_id) + query = query.order_by(_desc(model.Activity.timestamp)) + query = query.limit(15) + activity_objects = query.all() + return model_dictize.activity_list_dictize(activity_objects, context) + + def recently_changed_packages_activity_list(context, data_dict): '''Return the activity stream of all recently added or changed packages. @@ -1829,6 +2015,22 @@ def group_activity_list_html(context, data_dict): activity_stream = group_activity_list(context, data_dict) return activity_streams.activity_list_to_html(context, activity_stream) +def organization_activity_list_html(context, data_dict): + '''Return a organization's activity stream as HTML. + + The activity stream is rendered as a snippet of HTML meant to be included + in an HTML page, i.e. it doesn't have any HTML header or footer. + + :param id: the id or name of the organization + :type id: string + + :rtype: string + + ''' + activity_stream = group_activity_list(context, data_dict) + return activity_streams.activity_list_to_html(context, activity_stream) + + def recently_changed_packages_activity_list_html(context, data_dict): '''Return the activity stream of all recently changed packages as HTML. diff --git a/ckan/logic/action/update.py b/ckan/logic/action/update.py index 5dfb365ba7a..bfee54b2ffb 100644 --- a/ckan/logic/action/update.py +++ b/ckan/logic/action/update.py @@ -511,6 +511,118 @@ def group_update(context, data_dict): return model_dictize.group_dictize(group, context) +def organization_update(context, data_dict): + '''Update an organization + + You must be authorized to edit the organization. + + Plugins may change the parameters of this function depending on the value + of the group's ``type`` attribute, see the ``IGroupForm`` plugin interface. + + For further parameters see ``group_create()``. + + :param id: the name or id of the group to update + :type id: string + + :returns: the updated group + :rtype: dictionary + + ''' + model = context['model'] + user = context['user'] + session = context['session'] + id = _get_or_bust(data_dict, 'id') + parent = context.get('parent', None) + + organization = model.Group.get(id) + context["organization"] = organization + if organization is None: + raise NotFound('Organization was not found.') + + schema = ckan.logic.schema.group_form_schema() + + _check_access('organization_update', context, data_dict) + + data, errors = _validate(data_dict, schema, context) + log.debug('organization_update validate_errs=%r user=%s organization=%s data_dict=%r', + errors, context.get('user'), + context.get('organization').name if context.get('organization') else '', + data_dict) + + if errors: + session.rollback() + raise ValidationError(errors) + + rev = model.repo.new_revision() + rev.author = user + + if 'message' in context: + rev.message = context['message'] + else: + rev.message = _(u'REST API: Update object %s') % data.get("name") + + organization = model_save.group_dict_save(data, context) + + if parent: + parent_organization = model.Group.get( parent ) + if parent_organization and not organization_group in organization.get_groups('organization'): + # Delete all of this groups memberships + current = session.query(model.Member).\ + filter(model.Member.table_id == group.id).\ + filter(model.Member.table_name == "group").all() + if current: + log.debug('Parents of organization %s deleted: %r', organization.name, + [membership.group.name for membership in current]) + for c in current: + session.delete(c) + member = model.Member(group=parent_organization, table_id=organization.id, table_name='group') + session.add(member) + log.debug('Organization %s is made child of organization %s', + organization.name, parent_organization.name) + + for item in plugins.PluginImplementations(plugins.IGroupController): + item.edit(organization) + + activity_dict = { + 'user_id': model.User.by_name(user.decode('utf8')).id, + 'object_id': organization.id, + 'activity_type': 'changed organization', + } + # Handle 'deleted' groups. + # When the user marks a group as deleted this comes through here as + # a 'changed' group activity. We detect this and change it to a 'deleted' + # activity. + if organization.state == u'deleted': + if session.query(ckan.model.Activity).filter_by( + object_id=organization.id, activity_type='deleted').all(): + # A 'deleted group' activity for this group has already been + # emitted. + # FIXME: What if the group was deleted and then activated again? + activity_dict = None + else: + # We will emit a 'deleted group' activity. + activity_dict['activity_type'] = 'deleted organization' + if activity_dict is not None: + activity_dict['data'] = { + 'organization': ckan.lib.dictization.table_dictize(organization, context) + } + activity_create_context = { + 'model': model, + 'user': user, + 'defer_commit':True, + 'session': session + } + _get_action('activity_create')(activity_create_context, activity_dict, + ignore_auth=True) + # TODO: Also create an activity detail recording what exactly changed + # in the group. + + if not context.get('defer_commit'): + model.repo.commit() + + return model_dictize.organization_dictize(organization, context) + + def user_update(context, data_dict): '''Update a user account. @@ -776,6 +888,25 @@ def group_update_rest(context, data_dict): return group_dict +def organization_update_rest(context, data_dict): + + model = context['model'] + id = _get_or_bust(data_dict, "id") + organization = model.Group.get(id) + context["organization"] = organization + context["allow_partial_update"] = True + dictized_organization = model_save.group_api_to_dict(data_dict, context) + + _check_access('organization_update_rest', context, dictized_organization) + + dictized_after = _get_action('organization_update')(context, dictized_organization) + + organization = context['organization'] + + organization_dict = model_dictize.group_to_api(organization, context) + + return organization_dict + def vocabulary_update(context, data_dict): '''Update a tag vocabulary. diff --git a/ckan/logic/auth/__init__.py b/ckan/logic/auth/__init__.py index 66062ead609..cd32d67ff2c 100644 --- a/ckan/logic/auth/__init__.py +++ b/ckan/logic/auth/__init__.py @@ -29,6 +29,9 @@ def get_resource_object(context, data_dict={}): def get_group_object(context, data_dict={}): return _get_object(context, data_dict, 'group', 'Group') +def get_organization_object(context, data_dict={}): + return _get_object(context, data_dict, 'organization', 'Group') + def get_user_object(context, data_dict={}): return _get_object(context, data_dict, 'user_obj', 'User') diff --git a/ckan/logic/auth/publisher/__init__.py b/ckan/logic/auth/publisher/__init__.py index 8b2c572eb46..3487234405d 100644 --- a/ckan/logic/auth/publisher/__init__.py +++ b/ckan/logic/auth/publisher/__init__.py @@ -5,6 +5,5 @@ def _groups_intersect( groups_A, groups_B ): of intersection > 0). If both are empty for now we will allow it """ ga = set(groups_A) gb = set(groups_B) - + return len( ga.intersection( gb ) ) > 0 - \ No newline at end of file diff --git a/ckan/logic/auth/publisher/create.py b/ckan/logic/auth/publisher/create.py index 441db409609..eafe7db2c4e 100644 --- a/ckan/logic/auth/publisher/create.py +++ b/ckan/logic/auth/publisher/create.py @@ -82,6 +82,40 @@ def package_relationship_create(context, data_dict): else: return {'success': True} +def organization_create(context, data_dict=None): + """ + Organization create permission. If an organization is provided, within + which we want to create an organization then we check that the user is + within that group. If not then we just say Yes for now although there + may be some approval issues elsewhere. + """ + model = context['model'] + user = context['user'] + + if not model.User.get(user): + return {'success': False, 'msg': _('User is not authorized to create groups') } + + if Authorizer.is_sysadmin(user): + return {'success': True} + + try: + # If the user is doing this within another group then we need to make sure that + # the user has permissions for this group. + organization = get_group_object( context ) + except logic.NotFound: + return { 'success' : True } + + userobj = model.User.get( user ) + if not userobj: + return {'success': False, 'msg': _('User %s not authorized to create organizations') % str(user)} + + authorized = _groups_intersect(userobj.get_groups('organization'), [organization]) + if not authorized: + return {'success': False, 'msg': _('User %s not authorized to create organizations') % str(user)} + else: + return {'success': True} + + def group_create(context, data_dict=None): """ Group create permission. If a group is provided, within which we want to create a group @@ -108,13 +142,14 @@ def group_create(context, data_dict=None): if not userobj: return {'success': False, 'msg': _('User %s not authorized to create groups') % str(user)} - authorized = _groups_intersect( userobj.get_groups('organization'), [group] ) + authorized = _groups_intersect( userobj.get_groups(), [group] ) if not authorized: return {'success': False, 'msg': _('User %s not authorized to create groups') % str(user)} else: return {'success': True} + def rating_create(context, data_dict): # No authz check in the logic function return {'success': True} @@ -141,6 +176,14 @@ def group_create_rest(context, data_dict): return group_create(context, data_dict) +def organization_create_rest(context, data_dict): + model = context['model'] + user = context['user'] + if user in (model.PSEUDO_USER__VISITOR, ''): + return {'success': False, 'msg': _('Valid API key needed to create an organization')} + + return organization_create(context, data_dict) + def activity_create(context, data_dict): user = context['user'] return {'success': Authorizer.is_sysadmin(user)} diff --git a/ckan/logic/auth/publisher/delete.py b/ckan/logic/auth/publisher/delete.py index ece1c178903..7d8b40dbdeb 100644 --- a/ckan/logic/auth/publisher/delete.py +++ b/ckan/logic/auth/publisher/delete.py @@ -75,6 +75,32 @@ def related_delete(context, data_dict): return {'success': True} +def organization_delete(context, data_dict): + """ + Organization delete permission. Checks that the user specified is + within the organization to be deleted and also have 'admin' capacity. + """ + model = context['model'] + user = context['user'] + + if not user: + return {'success': False, 'msg': _('Only members of this organization are authorized to delete this group')} + + if Authorizer.is_sysadmin(user): + return {'success': True} + + organization = get_group_object(context, data_dict) + userobj = model.User.get(user) + if not userobj: + return {'success': False, 'msg': _('Only members of this group are authorized to delete this group')} + + authorized = _groups_intersect( userobj.get_groups('organization', 'admin'), [organization] ) + if not authorized: + return {'success': False, + 'msg': _('User %s not authorized to delete organization %s') % (str(user),organization.id)} + else: + return {'success': True} + def group_delete(context, data_dict): """ @@ -92,7 +118,7 @@ def group_delete(context, data_dict): if not userobj: return {'success': False, 'msg': _('Only members of this group are authorized to delete this group')} - authorized = _groups_intersect( userobj.get_groups('organization', 'admin'), [group] ) + authorized = _groups_intersect( userobj.get_groups(None, 'admin'), [group] ) if not authorized: return {'success': False, 'msg': _('User %s not authorized to delete group %s') % (str(user),group.id)} else: diff --git a/ckan/logic/auth/publisher/get.py b/ckan/logic/auth/publisher/get.py index 0e936c95105..802343708c7 100644 --- a/ckan/logic/auth/publisher/get.py +++ b/ckan/logic/auth/publisher/get.py @@ -1,6 +1,7 @@ import ckan.logic as logic -from ckan.logic.auth import get_package_object, get_group_object, \ - get_user_object, get_resource_object, get_related_object +from ckan.logic.auth import (get_package_object, get_group_object, + get_user_object, get_resource_object, + get_related_object, get_organization_object) from ckan.lib.base import _ from ckan.logic.auth.publisher import _groups_intersect from ckan.authz import Authorizer @@ -33,6 +34,10 @@ def revision_list(context, data_dict): def group_revision_list(context, data_dict): return group_show(context, data_dict) +def organization_revision_list(context, data_dict): + return organization_show(context, data_dict) + + def package_revision_list(context, data_dict): return package_show(context, data_dict) @@ -40,12 +45,23 @@ def group_list(context, data_dict): # List of all active groups is visible by default return {'success': True} +def organization_list(context, data_dict): + # List of all active organizations is visible by default + return {'success': True} + def group_list_authz(context, data_dict): return group_list(context, data_dict) def group_list_available(context, data_dict): return group_list(context, data_dict) +def organization_list_authz(context, data_dict): + return organization_list(context, data_dict) + +def organization_list_available(context, data_dict): + return organization_list(context, data_dict) + + def licence_list(context, data_dict): # Licences list is visible by default return {'success': True} @@ -124,6 +140,25 @@ def revision_show(context, data_dict): # No authz check in the logic function return {'success': True} +def organization_show(context, data_dict): + """ Organization show permission checks the user organization if the state is deleted """ + model = context['model'] + user = context.get('user') + org = get_organization_object(context, data_dict) + userobj = model.User.get( user ) + if Authorizer().is_sysadmin(unicode(user)): + return {'success': True} + + if org.state == 'deleted': + if not user or \ + not _groups_intersect(userobj.get_groups('organization'), org.get_groups('organization')): + return {'success': False, + 'msg': _('User %s not authorized to show organization %s') % (str(user), org.id)} + + return {'success': True} + + + def group_show(context, data_dict): """ Group show permission checks the user group if the state is deleted """ model = context['model'] @@ -135,7 +170,7 @@ def group_show(context, data_dict): if group.state == 'deleted': if not user or \ - not _groups_intersect( userobj.get_groups('organization'), group.get_groups('organization') ): + not _groups_intersect( userobj.get_groups(), group.get_groups() ): return {'success': False, 'msg': _('User %s not authorized to show group %s') % (str(user),group.id)} return {'success': True} @@ -155,6 +190,9 @@ def package_autocomplete(context, data_dict): def group_autocomplete(context, data_dict): return group_list(context, data_dict) +def organization_autocomplete(context, data_dict): + return organization_list(context, data_dict) + def tag_autocomplete(context, data_dict): return tag_list(context, data_dict) @@ -178,6 +216,10 @@ def package_show_rest(context, data_dict): def group_show_rest(context, data_dict): return group_show(context, data_dict) +def organization_show_rest(context, data_dict): + return organization_show(context, data_dict) + + def tag_show_rest(context, data_dict): return tag_show(context, data_dict) diff --git a/ckan/logic/auth/publisher/update.py b/ckan/logic/auth/publisher/update.py index 94abc58196c..1cdd482699c 100644 --- a/ckan/logic/auth/publisher/update.py +++ b/ckan/logic/auth/publisher/update.py @@ -1,6 +1,7 @@ import ckan.logic as logic -from ckan.logic.auth import get_package_object, get_group_object, \ - get_user_object, get_resource_object, get_related_object +from ckan.logic.auth import (get_package_object, get_group_object, + get_user_object, get_resource_object, + get_organization_object, get_related_object) from ckan.logic.auth.publisher import _groups_intersect from ckan.logic.auth.publisher.create import package_relationship_create from ckan.authz import Authorizer @@ -60,6 +61,28 @@ def package_edit_permissions(context, data_dict): return {'success': False, 'msg': _('Package edit permissions is not available')} +def organization_add_dataset(context, data_dict): + """ + Can the user add a dataset to the organization + """ + model = context['model'] + user = context.get('user','') + group = get_group_object(context, data_dict) + + if Authorizer().is_sysadmin(unicode(user)): + return { 'success': True } + + userobj = model.User.get( user ) + if not userobj: + return { 'success' : False, 'msg': _('Could not find user %s') % str(user) } + + # Any member of this group can add a package to it. + if not _groups_intersect(userobj.get_groups( 'organization' ), [group]): + return { 'success': False, 'msg': _('User %s not authorized to add datasets to this organization') % str(user) } + + return { 'success': True } + + def group_update(context, data_dict): """ Group edit permission. Checks that a valid user is supplied and that the user is @@ -82,11 +105,39 @@ def group_update(context, data_dict): return { 'success' : False, 'msg': _('Could not find user %s') % str(user) } # Only admins of this group should be able to update this group - if not _groups_intersect( userobj.get_groups( 'organization', 'admin' ), [group] ): + if not _groups_intersect( userobj.get_groups(None, 'admin' ), [group] ): return { 'success': False, 'msg': _('User %s not authorized to edit this group') % str(user) } return { 'success': True } +def organization_update(context, data_dict): + """ + Organization edit permission. Checks that a valid user is supplied and that the user is + a member of the group currently with admin capacity. + """ + model = context['model'] + user = context.get('user','') + organization = get_organization_object(context, data_dict) + + if not user: + return {'success': False, 'msg': _('Only members of this organization are authorized to edit this group')} + + # Sys admins should be allowed to update groups + if Authorizer().is_sysadmin(unicode(user)): + return { 'success': True } + + # Only allow package update if the user and package groups intersect + userobj = model.User.get( user ) + if not userobj: + return { 'success' : False, 'msg': _('Could not find user %s') % str(user) } + + # Only admins of this group should be able to update this group + if not _groups_intersect( userobj.get_groups( 'organization', 'admin' ), [organization] ): + return { 'success': False, 'msg': _('User %s not authorized to edit this organization') % str(user) } + + return { 'success': True } + + def related_update(context, data_dict): model = context['model'] user = context['user'] @@ -103,9 +154,15 @@ def related_update(context, data_dict): def group_change_state(context, data_dict): return group_update(context, data_dict) +def organization_change_state(context, data_dict): + return organization_update(context, data_dict) + def group_edit_permissions(context, data_dict): return {'success': False, 'msg': _('Group edit permissions is not implemented')} +def organization_edit_permissions(context, data_dict): + return {'success': False, 'msg': _('Organization edit permissions is not implemented')} + def user_update(context, data_dict): model = context['model'] user = context['user'] @@ -175,3 +232,11 @@ def group_update_rest(context, data_dict): return group_update(context, data_dict) +def organization_update_rest(context, data_dict): + model = context['model'] + user = context['user'] + if user in (model.PSEUDO_USER__VISITOR, ''): + return {'success': False, 'msg': _('Valid API key needed to edit a group')} + + return organization_update(context, data_dict) + diff --git a/ckan/logic/auth/update.py b/ckan/logic/auth/update.py index 7e5a9cc135d..da05beb772b 100644 --- a/ckan/logic/auth/update.py +++ b/ckan/logic/auth/update.py @@ -71,6 +71,9 @@ def package_edit_permissions(context, data_dict): else: return {'success': True} +def organization_add_dataset(context, data_dict): + return group_update(context, data_dict) + def group_update(context, data_dict): model = context['model'] user = context['user'] diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js index 450e71b8f5a..05027939c15 100644 --- a/ckan/public/scripts/application.js +++ b/ckan/public/scripts/application.js @@ -77,7 +77,7 @@ CKAN.Utils = CKAN.Utils || {}; $('#save').val(CKAN.Strings.addDataset); $("#title").focus(); } - var isGroupNew = $('body.group.new').length > 0; + var isGroupNew = $('body.group.new').length;; if (isGroupNew) { // Set up magic URL slug editor var urlEditor = new CKAN.View.UrlEditor({ @@ -87,6 +87,16 @@ CKAN.Utils = CKAN.Utils || {}; $("#title").focus(); } + var isOrganizationNew = $('body.organization.new').length;; + if (isOrganizationNew) { + // Set up magic URL slug editor + var urlEditor = new CKAN.View.UrlEditor({ + slugType: 'group' + }); + $('#save').val(CKAN.Strings.addOrganization); + $("#title").focus(); + } + var isDatasetEdit = $('body.package.edit').length > 0; if (isDatasetEdit) { CKAN.Utils.warnOnFormChanges($('form#dataset-edit')); diff --git a/ckan/templates/organizations/base_form_page.html b/ckan/templates/organizations/base_form_page.html new file mode 100644 index 00000000000..3d244e8f58d --- /dev/null +++ b/ckan/templates/organizations/base_form_page.html @@ -0,0 +1,29 @@ +{% extends "page.html" %} + +{% block breadcrumb_content %} +
  • {{ h.nav_link(_('Organizations'), controller='organization', action='index') }}
  • +
  • {% block breadcrumb_link %}{{ h.nav_link(_('Add an Organization'), controller='organization', action='new') }}{% endblock %}
  • +{% endblock %} + +{% block primary_content %} +
    +
    +

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

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

    {{ _('What are Organizations?') }}

    +
    + {% trans %} +

    Organizations are groups that are capable of containing both users and datasets, and also provide tools for managing them

    +

    It is possible within an organization to limit the visibility of datasets by setting the datasets to private within the organization.

    + {% endtrans %} +
    +
    +{% endblock %} diff --git a/ckan/templates/organizations/email/join_publisher_request.txt b/ckan/templates/organizations/email/join_publisher_request.txt new file mode 100644 index 00000000000..c15b46e0c54 --- /dev/null +++ b/ckan/templates/organizations/email/join_publisher_request.txt @@ -0,0 +1,12 @@ +Dear administrator, + +A request has been made for membership of your organization $group.title by $requester.name {% if requester.fullname %}( $requester.fullname ){% end %} + +The reason given for the request was: + +"$reason" + +Please contact the user to verify and then if you would like to add this user you can do so by visiting ${h.url_for(controller='ckanext.organizers.controllers:OrganizationController', action='users', id=group.name, qualified=True) } + +If you do not wish to add this user you can safely disregard this email. + diff --git a/ckan/templates/organizations/form.html b/ckan/templates/organizations/form.html new file mode 100644 index 00000000000..7a23a2dee07 --- /dev/null +++ b/ckan/templates/organizations/form.html @@ -0,0 +1,42 @@ +{% extends "organization/snippets/organization_form.html" %} + +{# +As the form is rendered as a seperate page we take advantage of this by +overriding the form blocks depending on the current context +#} +{% block dataset_fields %} + {% if action == "edit" %}{{ super() }}{% endif %} +{% endblock %} + +{% block custom_fields %} + {% if action == "edit" %}{{ super() }}{% endif %} +{% endblock %} + +{% block save_text %} + {%- if action == "edit" -%} + {{ _('Update Organization') }} + {%- else -%} + {{ _('Create Organization') }} + {%- endif -%} +{% endblock %} + +{% block delete_button %} + {% if action == "edit" %}{{ super() }}{% endif %} +{% endblock %} + +{% block basic_fields %} + {% set attrs = {'data-module': 'slug-preview-target'} %} + {{ form.input('title', label=_('Title'), id='field-title', placeholder=_('My Organization'), value=data.title, error=errors.title, classes=['control-full'], attrs=attrs) }} + + {% set prefix = h.url_for(controller='organization', action='read', id='') %} + {% set domain = h.url_for(controller='organization', action='read', id='', qualified=true) %} + {% set domain = domain|replace("http://", "")|replace("https://", "") %} + {% set attrs = {'data-module': 'slug-preview-slug', 'data-module-prefix': domain, 'data-module-placeholder': ''} %} + + {{ form.prepend('name', label=_('URL'), prepend=prefix, id='field-url', placeholder=_('my-organization'), value=data.name, error=errors.name, attrs=attrs) }} + + {{ form.markdown('description', label=_('Description'), id='field-description', placeholder=_('A little information about my organization...'), value=data.description, error=errors.description) }} + + {{ form.input('image_url', label=_('Image URL'), id='field-image-url', type='url', placeholder=_('http://example.com/my-image.jpg'), value=data.image_url, error=errors.image_url, classes=['control-full']) }} + + {% endblock %} \ No newline at end of file diff --git a/ckan/templates/organizations/index.html b/ckan/templates/organizations/index.html new file mode 100644 index 00000000000..8854b21a231 --- /dev/null +++ b/ckan/templates/organizations/index.html @@ -0,0 +1,41 @@ +{% extends "page.html" %} + +{% block subtitle %}{{ _('Organization') }}{% endblock %} + +{% block breadcrumb_content %} +
  • {% link_for _('Organizations'), controller='organization', action='index' %}
  • +{% endblock %} + +{% block actions_content %} +
  • {% link_for _('Add Organization'), controller='organization', action='new', class_='btn', icon='plus' %}
  • +{% endblock %} + +{% block primary_content %} +
    +
    +

    {{ _('Organizations') }}

    + {% if c.page.items %} + {% snippet "organization/snippets/organization_list.html", organizations=c.page.items %} + {% else %} +

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

    + {% endif %} +
    + {{ c.page.pager() }} +
    +{% endblock %} + +{% block secondary_content %} +
    +

    {{ _('What are Organizations?') }}

    +
    + {% trans %} +

    Organizations allow you to add both users and datasets to it in order to control who can access and manage your datasets

    + {% endtrans %} +
    +
    +{% endblock %} diff --git a/ckan/templates/organizations/new.html b/ckan/templates/organizations/new.html new file mode 100644 index 00000000000..7a2c2568120 --- /dev/null +++ b/ckan/templates/organizations/new.html @@ -0,0 +1,19 @@ +{% extends "organization/base_form_page.html" %} + +{% block subtitle %}{{ _('Create an organization') }}{% endblock %} + +{% block breadcrumb_link %}{{ h.nav_link(_('Create Organization'), controller='organization', action='edit', id=c.organization.name) }}{% endblock %} + +{% block page_heading %}{{ _('Create an organization') }}{% endblock %} + +{% block secondary_content %} +
    +

    {{ _('What are Organizations?') }}

    +
    + {% trans %} +

    An organization can be set-up to specify which users have permission to add or + remove datasets from it.

    + {% endtrans %} +
    +
    +{% endblock %} diff --git a/ckan/templates/organizations/read.html b/ckan/templates/organizations/read.html new file mode 100644 index 00000000000..d46ea42d0d6 --- /dev/null +++ b/ckan/templates/organizations/read.html @@ -0,0 +1,47 @@ +{% extends "page.html" %} + +{% block subtitle %}{{ c.organization_dict.display_name }}{% endblock %} + +{% block breadcrumb_content %} +
  • {% link_for _('Organizations'), controller='organization', action='index' %}
  • +
  • {% link_for c.organization_dict.display_name|truncate(35), controller='organization', action='read', id=c.organization_dict.name %}
  • +{% endblock %} + +{% block actions_content %} + {% if h.check_access('organization_update', {'id': c.organization.id}) %} +
  • {% link_for _('Add Dataset to Organization'), controller='package', action='new', organization=c.organization_dict.id, class_='btn', icon='plus' %}
  • +
  • {% link_for _('Edit'), controller='organization', action='edit', id=c.organization_dict.name, class_='btn', icon='cog' %}
  • +
  • {% link_for _('Manage Users'), controller='ckanext.organizations.controllers:OrganizationController', action='users', id=c.organization_dict.name, class_='btn', icon='cog' %}
  • + {% endif %} + {#
  • {% link_for _('History'), controller='organization', action='history', id=c.organization_dict.name, class_='btn', icon='undo' %}
  • #} +{% endblock %} + +{% block primary_content %} +
    +
    + {% include "package/snippets/search_form.html" %} +
    + {{ c.page.pager(q=c.q) }} +
    +{% endblock %} + +{% block secondary_content %} + {% snippet 'snippets/organization.html', organization=c.organization_dict %} + +
    +

    {{ _('Administrators') }}

    + +
    + + {{ h.snippet('snippets/facet_list.html', title='Tags', name='tags', extras={'id':c.organization_dict.id}) }} + {{ h.snippet('snippets/facet_list.html', title='Formats', name='res_format', extras={'id':c.organization_dict.id}) }} +{% endblock %} + +{% block links %} + {{ super() }} + {% include "organization/snippets/feeds.html" %} +{% endblock %} diff --git a/ckan/templates/organizations/snippets/feeds.html b/ckan/templates/organizations/snippets/feeds.html new file mode 100644 index 00000000000..f960eb216b7 --- /dev/null +++ b/ckan/templates/organizations/snippets/feeds.html @@ -0,0 +1,4 @@ +{%- set dataset_feed = h.url(controller='feed', action='group', id=c.group_dict.name) -%} +{%- set history_feed = h.url(controller='revision', action='list', format='atom', days=1) -%} + + diff --git a/ckan/templates/organizations/snippets/organization_form.html b/ckan/templates/organizations/snippets/organization_form.html new file mode 100644 index 00000000000..5060f348318 --- /dev/null +++ b/ckan/templates/organizations/snippets/organization_form.html @@ -0,0 +1,81 @@ +{% import 'macros/form.html' as form %} + +
    + {% block error_summary %} + {{ form.errors(error_summary) }} + {% endblock %} + + {% block basic_fields %} + {% set attrs = {'data-module': 'slug-preview-target'} %} + {{ form.input('title', label=_('Title'), id='field-title', placeholder=_('My Organization'), value=data.title, error=errors.title, classes=['control-full'], attrs=attrs) }} + + {# Perhaps these should be moved into the controller? #} + {% set prefix = h.url_for(controller='organization', action='read', id='') %} + {% set domain = h.url_for(controller='organization', action='read', id='', qualified=true) %} + {% set domain = domain|replace("http://", "")|replace("https://", "") %} + {% set attrs = {'data-module': 'slug-preview-slug', 'data-module-prefix': domain, 'data-module-placeholder': ''} %} + + {{ form.prepend('name', label=_('URL'), prepend=prefix, id='field-url', placeholder=_('my-organization'), value=data.name, error=errors.name, attrs=attrs) }} + + {{ form.markdown('description', label=_('Description'), id='field-description', placeholder=_('A little information about my organization...'), value=data.description, error=errors.description) }} + + {{ form.input('image_url', label=_('Image URL'), id='field-image-url', type='url', placeholder=_('http://example.com/my-image.jpg'), value=data.image_url, error=errors.image_url, classes=['control-full']) }} + + {% endblock %} + + {% block custom_fields %} + {% for extra in data.extras %} + {% set prefix = 'extras__%d__' % loop.index0 %} + {{ form.custom( + names=(prefix ~ 'key', prefix ~ 'value', prefix ~ 'deleted'), + id='field-extras-%d' % loop.index, + label=_('Custom Field'), + values=(extra.key, extra.value, extra.deleted), + error=errors[prefix ~ 'key'] or errors[prefix ~ 'value'] + ) }} + {% endfor %} + + {# Add a max if 3 empty columns #} + {% for extra in range(data.extras|count, 3) %} + {% set index = (loop.index0 + data.extras|count) %} + {% set prefix = 'extras__%d__' % index %} + {{ form.custom( + names=(prefix ~ 'key', prefix ~ 'value', prefix ~ 'deleted'), + id='field-extras-%d' % index, + label=_('Custom Field'), + values=(extra.key, extra.value, extra.deleted), + error=errors[prefix ~ 'key'] or errors[prefix ~ 'value'] + ) }} + {% endfor %} + {% endblock %} + + {% block dataset_fields %} + {% if data.packages %} +
    + +
    + {% for dataset in data.packages %} + + {% endfor %} +
    +
    + {% endif %} + + {% set dataset_name = 'packages__%s__name' % data.packages|length %} + {% set dataset_attrs = {'data-module': 'autocomplete', 'data-module-source': '/dataset/autocomplete?q=?'} %} + {{ form.input(dataset_name, label=_('Add Dataset'), id="field-dataset", value=data[dataset_name], classes=['control-medium'], attrs=dataset_attrs) }} + {% endblock %} + +
    + {% block delete_button %} + {% if h.check_access('organization_delete', {'id': data.id}) %} + {% set locale = h.dump_json({'content': _('Are you sure you want to delete this Organization?')}) %} + {% block delete_button_text %}{{ _('Delete') }}{% endblock %} + {% endif %} + {% endblock %} + +
    + diff --git a/ckan/templates/organizations/snippets/organization_item.html b/ckan/templates/organizations/snippets/organization_item.html new file mode 100644 index 00000000000..ff1add979c9 --- /dev/null +++ b/ckan/templates/organizations/snippets/organization_item.html @@ -0,0 +1,38 @@ +{# +Renders a media item for a organization. This should be used in a list. + +organization - A organization dict. +first - Pass true if this is the first item in a row. +last - Pass true if this is the last item in a row. + +Example: + +
      + {% for organization in organizations %} + {% set first = loop.index0 % 3 == 0 %} + {% set last = loop.index0 % 3 == 2 %} + {% snippet "organization/snippets/organization_item.html", organization=organization, first=first, last=last %} + {% endfor %} +
    +#} +{% set url = h.url_for('organization_read', action='read', id=organization.name) %} +
  • + {{ organization.name }} +
    +

    + + {{ organization.display_name }} + +

    + {% if organization.description %} +

    {{ h.truncate(organization.description, length=80, whole_word=True) }}

    + {% else %} +

    {{ _('This organization has no description') }}

    + {% endif %} + {% if organization.packages %} + {{ ungettext('{num} Dataset', '{num} Datasets', organization.packages).format(num=organization.packages) }} + {% else %} + {{ _('0 Datasets') }} + {% endif %} +
    +
  • diff --git a/ckan/templates/organizations/snippets/organization_list.html b/ckan/templates/organizations/snippets/organization_list.html new file mode 100644 index 00000000000..3ed7a12d3e5 --- /dev/null +++ b/ckan/templates/organizations/snippets/organization_list.html @@ -0,0 +1,17 @@ +{# +Display a grid of organization items. + +organizations - A list of organizations. + +Example: + + {% snippet "organization/snippets/organization_list.html" %} + +#} +
      + {% for organization in organizations %} + {% set first = loop.index0 % 3 == 0 %} + {% set last = loop.index0 % 3 == 2 %} + {% snippet "organization/snippets/organization_item.html", organization=organization, first=first, last=last %} + {% endfor %} +
    diff --git a/ckan/templates/organizations/users.html b/ckan/templates/organizations/users.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ckan/templates/organizations/users_form.html b/ckan/templates/organizations/users_form.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ckan/templates_legacy/_util.html b/ckan/templates_legacy/_util.html index 1c69831dc58..4e5f18cf5ca 100644 --- a/ckan/templates_legacy/_util.html +++ b/ckan/templates_legacy/_util.html @@ -89,6 +89,22 @@ + + + + + + + + + + + + +
    LogoTitleNumber of datasetsNumber of usersDescription
    ${organization['display_name']}${organization['packages']}${organization['users']}${h.truncate(organization['description'], length=80, whole_word=True)}
    + + - /* - * Included as inline javascript in layout_base.html. Simpler than - * trying to trick the translation system into reading a js file. + /* + * Included as inline javascript in layout_base.html. Simpler than + * trying to trick the translation system into reading a js file. */ var CKAN = CKAN || {}; CKAN.Strings = CKAN.Strings || {}; @@ -23,6 +23,7 @@ failedToSave = _('Failed to save, possibly due to invalid data '), addDataset = _('Add Dataset'), addGroup = _('Add Group'), + addOrganization = _('Add Organization'), youHaveUnsavedChanges = _("You have unsaved changes. Make sure to click 'Save Changes' below before leaving this page."), loading = _('Loading...'), noNameBrackets = _('(no name)'), diff --git a/ckan/templates_legacy/organization/__init__.py b/ckan/templates_legacy/organization/__init__.py new file mode 100644 index 00000000000..b646540d6be --- /dev/null +++ b/ckan/templates_legacy/organization/__init__.py @@ -0,0 +1 @@ +# empty file needed for pylons to find templates in this directory diff --git a/ckan/templates_legacy/organization/apply.html b/ckan/templates_legacy/organization/apply.html new file mode 100644 index 00000000000..d88eee54370 --- /dev/null +++ b/ckan/templates_legacy/organization/apply.html @@ -0,0 +1,15 @@ + + + Apply + Apply for membership + + +
    + ${Markup(c.form)} +
    + + + + diff --git a/ckan/templates_legacy/organization/apply_form.html b/ckan/templates_legacy/organization/apply_form.html new file mode 100644 index 00000000000..a556a8b1a0e --- /dev/null +++ b/ckan/templates_legacy/organization/apply_form.html @@ -0,0 +1,49 @@ + +
    + +
    +

    Errors in form

    +

    The form contains invalid entries:

    +
      +
    • ${"%s: %s" % (key, error)}
    • +
    +
    + + + +
    +
    +
    + +
    +
    + +
    + + +
    +
    + +
    + Please explain to the owner your reasons for wishing to become an editor of this organization +
    +
    +
    + + +
    + + + + +
    +
    diff --git a/ckan/templates_legacy/organization/edit.html b/ckan/templates_legacy/organization/edit.html new file mode 100644 index 00000000000..9ee848b71cc --- /dev/null +++ b/ckan/templates_legacy/organization/edit.html @@ -0,0 +1,16 @@ + + + Edit: ${c.organization.display_name} + Edit: ${c.organization.display_name} + ${h.literal('no-sidebar')} + + +
    + ${Markup(c.form)} +
    + + + + diff --git a/ckan/templates_legacy/organization/email/join_publisher_request.txt b/ckan/templates_legacy/organization/email/join_publisher_request.txt new file mode 100644 index 00000000000..99476789af2 --- /dev/null +++ b/ckan/templates_legacy/organization/email/join_publisher_request.txt @@ -0,0 +1,12 @@ +Dear administrator, + +A request has been made for membership of your organization $group.title by $requester.name {% if requester.fullname %}( $requester.fullname ){% end %} + +The reason given for the request was: + +"$reason" + +Please contact the user to verify and then if you would like to add this user you can do so by visiting ${h.url_for(controller='organization', action='users', id=organization.name, qualified=True) } + +If you do not wish to add this user you can safely disregard this email. + diff --git a/ckan/templates_legacy/organization/history.html b/ckan/templates_legacy/organization/history.html new file mode 100644 index 00000000000..e7e32b0535e --- /dev/null +++ b/ckan/templates_legacy/organization/history.html @@ -0,0 +1,59 @@ + + + History: ${c.organization.display_name} + History: ${c.organization.display_name} + +
    +

    + Revisions + +

    +
    + +

    + Error: ${c.error} +

    + + + + + + + + + + + + + + + + +
    RevisionTimestampAuthorLog Message
    + ${h.radio("selected1", revision_dict['id'], checked=(index == 0))} + ${h.radio("selected2", revision_dict['id'], checked=(index == len(c.organization_revisions)-1))} + + ${revision_dict['id']} + ${revision_dict['timestamp']}${h.linked_user(revision_dict['author'])}${revision_dict['message']}
    + +
    +
    + + + + + + + diff --git a/ckan/templates_legacy/organization/index.html b/ckan/templates_legacy/organization/index.html new file mode 100644 index 00000000000..46d3d65d913 --- /dev/null +++ b/ckan/templates_legacy/organization/index.html @@ -0,0 +1,24 @@ + + + Organization + Organization + + +
  • +

    What Are Organizations?

    + Organizations are groups of users and datasets where they can be set-up to specify which users have permission to add or remove datasets from it. +
  • +
    + + +
    + ${c.page.pager()} + ${organization_list_from_dict(c.page.items)} + ${c.page.pager()} +
    + + + diff --git a/ckan/templates_legacy/organization/layout.html b/ckan/templates_legacy/organization/layout.html new file mode 100644 index 00000000000..8fc2b0c7212 --- /dev/null +++ b/ckan/templates_legacy/organization/layout.html @@ -0,0 +1,46 @@ + + + + + + + + diff --git a/ckan/templates_legacy/organization/new.html b/ckan/templates_legacy/organization/new.html new file mode 100644 index 00000000000..69f1b76a817 --- /dev/null +++ b/ckan/templates_legacy/organization/new.html @@ -0,0 +1,14 @@ + + + Add An Organization + Add An Organization + +
    + ${Markup(c.form)} +
    + + + + diff --git a/ckan/templates_legacy/organization/organization_form.html b/ckan/templates_legacy/organization/organization_form.html new file mode 100644 index 00000000000..de1dbcd080b --- /dev/null +++ b/ckan/templates_legacy/organization/organization_form.html @@ -0,0 +1,123 @@ +
    + + + +
    +

    Errors in form

    +

    The form contains invalid entries:

    +
      +
    • ${"%s: %s" % (key if not key=='Name' else 'URL', error)}
    • +
    +
    + +
    +
    + +
    + +
    +
    +
    + +
    +
    + ${h.url(controller='organization', action='index')+'/'} + +
    +

     

    +

    Warning: URL is very long. Consider changing it to something shorter.

    +

    2+ characters, lowercase, using only 'a-z0-9' and '-_'

    +

    ${errors.get('name', '')}

    +
    +
    +
    + +
    + ${markdown_editor('description', data.get('description'), 'notes', _('Start with a summary sentence ...'))} +
    +
    +
    + +
    + +

    The URL for the image that is associated with this organization.

    +
    +
    +
    + +
    + +
    +
    +
    + +
    +

    Extras

    +
    + + +
    + +
    + + + +
    +
    +
    +
    + +
    + +
    + + +
    +
    +
    +
    +
    +
    + +
    +

    Datasets

    +
    + +
    +
    +
    + + +
    +
    +
    +
    +
    +

    There are no datasets currently in this organization.

    +
    + +
    + + + + +
    + diff --git a/ckan/templates_legacy/organization/read.html b/ckan/templates_legacy/organization/read.html new file mode 100644 index 00000000000..630858b313f --- /dev/null +++ b/ckan/templates_legacy/organization/read.html @@ -0,0 +1,81 @@ + + + + + ${c.organization_dict.display_name} + ${c.organization_dict.display_name} + + ${c.organization.image_url} + + + + +
  • +
      + +
    • +

      Administrators

      +
        +
      • ${h.linked_user(admin)}
      • +
      +
    • +
      +
    +
  • + +
  • +
      + +
    • +

      Members

      +
        +
      • ${h.linked_user(user)}
      • +
      +
    • +
      +
    +
  • + + + ${facet_div('tags', _('Tags'))} + ${facet_div('res_format', _('Resource Formats'))} +
    + + +

    State: ${c.organization['state']}

    +
    +
    + ${c.description_formatted} +
    +
    + +
    +
    +

    Datasets

    + + ${field_list()} + +

    You searched for "${c.q}". ${c.page.item_count} datasets found.

    + ${c.page.pager()} + ${package_list_from_dict(c.page.items)} + ${c.page.pager()} +
    +
    + + + + + + + + + + diff --git a/ckan/templates_legacy/organization/users.html b/ckan/templates_legacy/organization/users.html new file mode 100644 index 00000000000..9b99d49c0c2 --- /dev/null +++ b/ckan/templates_legacy/organization/users.html @@ -0,0 +1,16 @@ + + + Users: ${c.organization.display_name} + Users: ${c.organization.display_name} + + +
    + ${Markup(c.form)} +
    + + + + + diff --git a/ckan/templates_legacy/organization/users_form.html b/ckan/templates_legacy/organization/users_form.html new file mode 100644 index 00000000000..1f4e7e17ba6 --- /dev/null +++ b/ckan/templates_legacy/organization/users_form.html @@ -0,0 +1,72 @@ +
    + +
    +

    Errors in form

    +

    The form contains invalid entries:

    +
      +
    • ${"%s: %s" % (key, error)}
    • +
    +
    + + + +
    +

    Users

    +
    + +
    + + + +
    + Admin + Editor + + +
    +
    +
    +
    +

    There are no users currently in this organization.

    + +

    Add users

    +
    +
    + +
    +
    +
    + + +
    + + + + +
    + + + +
    + diff --git a/ckan/tests/misc/test_auth_profiles.py b/ckan/tests/misc/test_auth_profiles.py deleted file mode 100644 index d8c9ee103ae..00000000000 --- a/ckan/tests/misc/test_auth_profiles.py +++ /dev/null @@ -1,67 +0,0 @@ -from ckan.tests import * -import ckan.forms -import ckan.model as model -from ckan.tests.pylons_controller import PylonsTestCase - -from pylons import config - -class TestAuthProfiles(PylonsTestCase): - - @classmethod - def setup_class(self): - model.repo.init_db() - - @classmethod - def teardown_class(self): - model.repo.rebuild_db() - - def test_load_publisher_profile(self): - """ Ensure that the relevant config settings result in the appropriate - functions being loaded from the correct module """ - from new_authz import is_authorized, _get_auth_function - - config['ckan.auth.profile'] = 'publisher' - _ = is_authorized('site_read', {'model': model, 'user': '127.0.0.1','reset_auth_profile':True}) - s = str(_get_auth_function('site_read').__module__) - assert s == 'ckan.logic.auth.publisher.get', s - - def test_authorizer_count(self): - """ Ensure that we have the same number of auth functions in the - core auth profile as in the publisher auth profile """ - - modules = { - 'ckan.logic.auth': 0, - 'ckan.logic.auth.publisher': 0 - } - - module_items = { - 'ckan.logic.auth': {}, - 'ckan.logic.auth.publisher': {} - } - - for module_root in modules.keys(): - for auth_module_name in ['get', 'create', 'update','delete']: - module_path = '%s.%s' % (module_root, auth_module_name,) - module = __import__(module_path) - - for part in module_path.split('.')[1:]: - module = getattr(module, part) - - for key, v in module.__dict__.items(): - # we should check these are actually functions - if not key.startswith('_') and key != 'ckan': - modules[module_root] = modules[module_root] + 1 - info = '%s.%s.%s' % (module_root, auth_module_name, key) - module_items[module_root][key] = info - - err = [] - if modules['ckan.logic.auth'] != modules['ckan.logic.auth.publisher']: - oldauth = module_items['ckan.logic.auth'] - pubauth = module_items['ckan.logic.auth.publisher'] - for e in oldauth: - if not e in pubauth: - err.append( '%s is in auth but not publisher auth ' % oldauth[e] ) - for e in pubauth: - if not e in oldauth: - err.append( '%s is in publisher auth but not auth ' % pubauth[e] ) - assert not err, '\n'.join(err)