diff --git a/ckan/lib/cli.py b/ckan/lib/cli.py index c7831b4e313..f1355633321 100644 --- a/ckan/lib/cli.py +++ b/ckan/lib/cli.py @@ -890,7 +890,6 @@ class DatasetCmd(CkanCommand): def command(self): self._load_config() - import ckan.model as model if not self.args: print self.usage @@ -939,13 +938,12 @@ def delete(self, dataset_ref): print '%s %s -> %s' % (dataset.name, old_state, dataset.state) def purge(self, dataset_ref): - import ckan.model as model dataset = self._get_dataset(dataset_ref) name = dataset.name - rev = model.repo.new_revision() - dataset.purge() - model.repo.commit_and_remove() + context = {'user': self.site_user['name']} + logic.get_action('dataset_purge')( + context, {'id': dataset_ref}) print '%s purged' % name diff --git a/ckan/logic/action/delete.py b/ckan/logic/action/delete.py index dd562201f19..44b26d64887 100644 --- a/ckan/logic/action/delete.py +++ b/ckan/logic/action/delete.py @@ -44,6 +44,9 @@ def user_delete(context, data_dict): def package_delete(context, data_dict): '''Delete a dataset (package). + This makes the dataset disappear from all web & API views, apart from the + trash. + You must be authorized to delete the dataset. :param id: the id or name of the dataset to delete @@ -73,6 +76,45 @@ def package_delete(context, data_dict): entity.delete() model.repo.commit() + +def dataset_purge(context, data_dict): + '''Purge a dataset. + + .. warning:: Purging a dataset cannot be undone! + + Purging a database completely removes the dataset from the CKAN database, + whereias deleting a dataset simply marks the dataset as deleted (it will no + longer show up in the front-end, but is still in the db). + + You must be authorized to purge the dataset. + + :param id: the name or id of the dataset to be purged + :type id: string + + ''' + model = context['model'] + id = _get_or_bust(data_dict, 'id') + + pkg = model.Package.get(id) + context['package'] = pkg + if pkg is None: + raise NotFound('Dataset was not found') + + _check_access('dataset_purge', context, data_dict) + + members = model.Session.query(model.Member) \ + .filter(model.Member.table_id == pkg.id) \ + .filter(model.Member.table_name == 'package') + if members.count() > 0: + for m in members.all(): + m.purge() + + pkg = model.Package.get(id) + model.repo.new_revision() + pkg.purge() + model.repo.commit_and_remove() + + def resource_delete(context, data_dict): '''Delete a resource from a dataset. diff --git a/ckan/logic/auth/delete.py b/ckan/logic/auth/delete.py index 1c94918c394..7978ac4dbbd 100644 --- a/ckan/logic/auth/delete.py +++ b/ckan/logic/auth/delete.py @@ -17,6 +17,10 @@ def package_delete(context, data_dict): # are essentially changing the state field return _auth_update.package_update(context, data_dict) +def dataset_purge(context, data_dict): + # Only sysadmins are authorized to purge datasets + return {'success': False} + def resource_delete(context, data_dict): model = context['model'] user = context.get('user') diff --git a/ckan/model/group.py b/ckan/model/group.py index 0882e9e245b..e9380b1c8ad 100644 --- a/ckan/model/group.py +++ b/ckan/model/group.py @@ -104,11 +104,11 @@ def related_packages(self): def __unicode__(self): # refer to objects by name, not ID, to help debugging if self.table_name == 'package': - table_info = 'package=%s' % meta.Session.query(_package.Package).\ - get(self.table_id).name + pkg = meta.Session.query(_package.Package).get(self.table_id) + table_info = 'package=%s' % pkg.name if pkg else 'None' elif self.table_name == 'group': - table_info = 'group=%s' % meta.Session.query(Group).\ - get(self.table_id).name + group = meta.Session.query(Group).get(self.table_id) + table_info = 'group=%s' % group.name if group else 'None' else: table_info = 'table_name=%s table_id=%s' % (self.table_name, self.table_id) diff --git a/ckan/tests/logic/action/test_delete.py b/ckan/tests/logic/action/test_delete.py index d8079968bee..826295cc9ad 100644 --- a/ckan/tests/logic/action/test_delete.py +++ b/ckan/tests/logic/action/test_delete.py @@ -140,3 +140,63 @@ def test_tag_delete_with_unicode_returns_unicode_error(self): assert u'Delta symbol: \u0394' in unicode(e) else: assert 0, 'Should have raised NotFound' + + +class TestDatasetPurge(object): + def setup(self): + helpers.reset_db() + + def test_a_non_sysadmin_cant_purge_dataset(self): + user = factories.User() + dataset = factories.Dataset(user=user) + + assert_raises(logic.NotAuthorized, + helpers.call_action, + 'dataset_purge', + context={'user': user['name'], 'ignore_auth': False}, + id=dataset['name']) + + def test_purged_dataset_does_not_show(self): + dataset = factories.Dataset() + + helpers.call_action('dataset_purge', + context={'ignore_auth': True}, + id=dataset['name']) + + assert_raises(logic.NotFound, helpers.call_action, 'package_show', + context={}, id=dataset['name']) + + def test_purged_dataset_leaves_no_trace_in_the_model(self): + factories.Group(name='group1') + dataset = factories.Dataset( + tags=[{'name': 'tag1'}], + groups=[{'name': 'group1'}], + extras=[{'key': 'testkey', 'value': 'testvalue'}]) + num_revisions_before = model.Session.query(model.Revision).count() + + helpers.call_action('dataset_purge', + context={'ignore_auth': True}, + id=dataset['name']) + num_revisions_after = model.Session.query(model.Revision).count() + + # the Package and related objects are gone + assert_equals(model.Session.query(model.Package).all(), []) + assert_equals(model.Session.query(model.PackageTag).all(), []) + # there is no clean-up of the tag object itself, just the PackageTag. + assert_equals([t.name for t in model.Session.query(model.Tag).all()], + ['tag1']) + assert_equals(model.Session.query(model.PackageExtra).all(), []) + # the only member left is the user created by factories.Group() + assert_equals([m.table_name + for m in model.Session.query(model.Member).all()], + ['user']) + + # all the object revisions were purged too + assert_equals(model.Session.query(model.PackageRevision).all(), []) + assert_equals(model.Session.query(model.PackageTagRevision).all(), []) + assert_equals(model.Session.query(model.PackageExtraRevision).all(), + []) + # Member is not revisioned + + # No Revision objects were purged, in fact 1 is created for the purge + assert_equals(num_revisions_after - num_revisions_before, 1)