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 %} +
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 %} ++ {{ _('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 %} +Organizations allow you to add both users and datasets to it in order to control who can access and manage your datasets
+ {% endtrans %} +An organization can be set-up to specify which users have permission to add or + remove datasets from it.
+ {% endtrans %} +{{ 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 %} +Logo | Title | Number of datasets | Number of users | Description |
---|---|---|---|---|
${organization['display_name']} | +${organization['packages']} | +${organization['users']} | +${h.truncate(organization['description'], length=80, whole_word=True)} | +
You searched for "${c.q}". ${c.page.item_count} datasets found.
+ ${c.page.pager()} + ${package_list_from_dict(c.page.items)} + ${c.page.pager()} +