Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7fea5ac
Dataset versions stubs and first tests.
tomeksabala May 25, 2021
1abbcc8
Added docstrings to method names.
tomeksabala May 26, 2021
11b433a
Test helper functions moved out from the class.
tomeksabala May 26, 2021
fdda61b
Added missing test stubs. Renamed some test methods.
tomeksabala May 26, 2021
8945197
Added tests implementation for dataset version latest.
tomeksabala May 30, 2021
32d8805
Fixed some minor bugs in the tests.
tomeksabala May 30, 2021
42ccd3c
WIP: Removed ForeignKey constraint for resource_id to allow nullable …
tomeksabala May 30, 2021
e68de95
dataset_version_create implementation
tomeksabala May 30, 2021
d14e2fb
dataset_version_list implementation
tomeksabala May 30, 2021
1cd8ade
dataset_version_latest implementation
tomeksabala May 30, 2021
30b53c6
activity_dataset_show implementation
tomeksabala May 30, 2021
54961ec
get_activity_id_from_dataset_version_name implementation
tomeksabala May 30, 2021
304205b
dataset_has_versions implementation
tomeksabala May 30, 2021
baf5a43
Defined dataset version actions in plugin def.
tomeksabala May 30, 2021
6339ef1
Support for custom activity_id in dataset_version_create
tomeksabala May 31, 2021
82ab9c4
Extracted fixtures to fixtures.py and common test helpers to __init__.py
tomeksabala May 31, 2021
f825de1
Added version_update to IActions.
tomeksabala May 31, 2021
e547ec4
Fix typos in version_update
tomeksabala May 31, 2021
72226e6
Added helper to get dataset version for activity_id.
tomeksabala May 31, 2021
8e211d6
Dataset version create accepts id or name.
tomeksabala Jun 1, 2021
04e5efd
Dataset version restore tests.
tomeksabala Jun 2, 2021
3d03661
Fixup, codestyle.
tomeksabala Jun 2, 2021
9487629
version_show supports version_id or version_name & dataset_id
tomeksabala Jun 2, 2021
ce79afb
dataset_version_restore implementation
tomeksabala Jun 2, 2021
694ddb2
Refactored checks for error messages to use `match` kwarg.
tomeksabala Jun 2, 2021
40a1678
Merge remote-tracking branch 'datopian/main' into ADX-645-dataset-ver…
tomeksabala Jun 4, 2021
99dc7eb
Merge remote-tracking branch 'datopian/main' into ADX-645-dataset-ver…
tomeksabala Jun 7, 2021
1ea6a63
ADX-680 Can't create multiple versions for single activity_id
tomeksabala Jun 7, 2021
36d9f37
ADX-689 Added a test to verify create by time order of version list.
tomeksabala Jun 7, 2021
73961a8
Codestyle.
tomeksabala Jun 7, 2021
ab248d7
Fix for version_update using legacy notes field name
tomeksabala Jun 8, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions ckanext/versions/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,25 @@ def download_url(resource_url, version_id):
)

return url


def dataset_version_for_activity_id(dataset_id, activity_id):
"""Return dataset version created for given activity.

:param dataset_id: the id or name of the dataset
:type dataset_id: string
:param activity_id: the id of the activity
:type activity_id: string
:returns: version, None if no version created for the given activity.
:rtype: dictionary
"""
# TODO: Think if instead `IPackageController.after_show` could be
# used to include version dict in package_dict
context = {'user': toolkit.g.user}
versions = toolkit.get_action('dataset_version_list')(
context, {'dataset_id': dataset_id}
)
for version in versions:
if version['activity_id'] == activity_id:
return version
return None
26 changes: 18 additions & 8 deletions ckanext/versions/logic/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@
def version_update(context, data_dict):
"""Update a version from the current dataset.

:param package_id: the id the dataset
:type package_id: string
:param version_id: the id of the version
:type version_id: string
:param name: A short name for the version
Expand All @@ -30,7 +28,7 @@ def version_update(context, data_dict):
:rtype: dictionary
"""
model = context.get('model', core_model)
version_id, name = toolkit.get_or_bust(data_dict, ['version', 'name'])
version_id, name = toolkit.get_or_bust(data_dict, ['version_id', 'name'])

# I'll create my own session! With Blackjack! And H**kers!
session = model.meta.create_local_session()
Expand All @@ -42,11 +40,11 @@ def version_update(context, data_dict):
if not version:
raise toolkit.ObjectNotFound('Version not found')

toolkit.check_access('dataset_version_create', context, data_dict)
toolkit.check_access('version_create', context, data_dict)
assert context.get('auth_user_obj') # Should be here after `check_access`

version.name = name
version.description = data_dict.get('description', None)
version.notes = data_dict.get('notes', None)

session.add(version)

Expand Down Expand Up @@ -228,14 +226,26 @@ def version_delete(context, data_dict):
def version_show(context, data_dict):
"""Show a specific version object

:param version_id: the id of the version
:param version_id: the id or name of the version
:type version_id: string
:param dataset_id: [Optional] the id or name of a dataset. Mandatory
if version name provided as version_id
:type dataset_id: string
:returns: the version dictionary
:rtype: dict
"""
model = context.get('model', core_model)
version_id = toolkit.get_or_bust(data_dict, ['version_id'])
version = model.Session.query(Version).get(version_id)
version_name_or_id = toolkit.get_or_bust(data_dict, ['version_id'])
version = model.Session.query(Version).get(version_name_or_id)
if not version:
version_name = version_name_or_id
dataset_name_or_id = data_dict.get('dataset_id')
dataset = model.Package.get(dataset_name_or_id)
if dataset:
dataset_id = dataset.id
version = model.Session.query(Version). \
filter(Version.package_id == dataset_id). \
filter(Version.name == version_name).one_or_none()
if not version:
raise toolkit.ObjectNotFound('Version not found')

Expand Down
226 changes: 226 additions & 0 deletions ckanext/versions/logic/dataset_version_action.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import logging

from datetime import datetime
from sqlalchemy.exc import IntegrityError

from ckan import model as core_model
from ckan.plugins import toolkit
from ckanext.versions.logic.action import version_show
from ckanext.versions.model import Version

log = logging.getLogger(__name__)


def dataset_version_create(context, data_dict):
"""Create a new version from the current dataset's activity_id

Currently you must have editor level access on the dataset
to create a version. If creator_user_id is not present, it will be set as
the logged it user.

:param dataset_id: the id of the dataset
:type dataset_id: string
:param name: A short name for the version, e.g. v1.0
:type name: string
:param notes optional: Notes about the version
:type notes: string
:returns: the newly created version
:rtype: dictionary
"""
model = context.get('model', core_model)
dataset_name_or_id, name = toolkit.get_or_bust(
data_dict, ['dataset_id', 'name'])
activity_id = data_dict.get('activity_id')

dataset = model.Package.get(dataset_name_or_id)
if not dataset:
raise toolkit.ObjectNotFound("Dataset not found")
dataset_id = dataset.id
toolkit.check_access('version_create',
context,
{"package_id": dataset_id})
creator_user_id = context['auth_user_obj'].id

if activity_id:
activity = model.Activity.get(activity_id)
else:
activity = model.Session.query(model.Activity). \
filter_by(object_id=dataset_id). \
order_by(model.Activity.timestamp.desc()). \
first()
if not activity:
raise toolkit.ObjectNotFound('Activity not found')

version_for_activity = model.Session.query(Version). \
filter_by(activity_id=activity.id). \
first()
if version_for_activity:
raise toolkit.ValidationError("Version already exists for this activity")

version = Version(
package_id=dataset_id,
activity_id=activity.id,
name=name,
notes=data_dict.get('notes', None),
created=datetime.utcnow(),
creator_user_id=creator_user_id)

model.Session.add(version)
try:
model.Session.commit()
except IntegrityError as e:
# Name not unique, or foreign key constraint violated
model.Session.rollback()
log.debug("DB integrity error (version name not unique?): %s", e)
raise toolkit.ValidationError(
'Version names must be unique per dataset'
)

log.info(
'Version "%s" created for dataset %s',
name,
dataset_id
)

return version.as_dict()


def dataset_version_restore(context, data_dict):
"""Restores dataset version by restoring dataset
metadata and creating a new version with it

:param dataset_id: the id or name of the dataset
:type dataset_id: string
:param version_id: the id or name of the dataset
:type version_id: string
:returns: restored dataset
:rtype: dict
"""
model = context.get('model', core_model)
dataset_name_or_id, version_name_or_id = toolkit.get_or_bust(
data_dict, ['dataset_id', 'version_id'])

dataset = model.Package.get(dataset_name_or_id)
if not dataset:
raise toolkit.ObjectNotFound("Dataset not found")
dataset_id = dataset.id
toolkit.check_access('version_create',
context,
{"package_id": dataset_id})

version = version_show(context, data_dict)
if not version:
raise toolkit.ObjectNotFound("Version not found")
v_name = "restored_{}".format(version['name'])
v_notes = "Restored from version: {}".format(version['name'])
old_dataset = activity_dataset_show(
context,
{
'activity_id': version['activity_id'],
'dataset_id': dataset_id
}
)
restored_dataset = toolkit.get_action('package_update')(context, old_dataset)
dataset_version_create(context, {'dataset_id': dataset.id, 'name': v_name, 'notes': v_notes})

return restored_dataset


@toolkit.side_effect_free
def dataset_version_list(context, data_dict):
"""List versions of a given dataset

:param dataset_id: the id of the dataset
:type dataset_id: string
:returns: list of versions created for the dataset
:rtype: list
"""
model = context.get('model', core_model)
dataset_id = toolkit.get_or_bust(data_dict, ['dataset_id'])
dataset = model.Package.get(dataset_id)
if not dataset:
raise toolkit.ObjectNotFound('Dataset not found')

toolkit.check_access('version_list', context,
{"package_id": dataset_id})

versions = model.Session.query(Version). \
filter(Version.package_id == dataset.id). \
order_by(Version.created.desc())

return [v.as_dict() for v in versions]


@toolkit.side_effect_free
def dataset_version_latest(context, data_dict):
''' Show the latest version for a dataset

:param dataset_id: the if of the dataset
:type dataset_id: string
:returns the version dictionary
:rtype dict
'''
version_list = dataset_version_list(context, data_dict)
if len(version_list) < 1:
raise toolkit.ObjectNotFound("Versions not found for this dataset")
return version_list[0]


def activity_dataset_show(context, data_dict):
''' Returns a dataset from the activity object.

:param activity_id: the id of the activity
:type activity_id: string
:param dataset_id: the id of the resource
:type dataset_id: string
:returns: The dataset in the activity
:rtype: dict
'''
activity_id, dataset_id = toolkit.get_or_bust(
data_dict,
['activity_id', 'dataset_id']
)
dataset = toolkit.get_action('activity_data_show')(
context,
{'id': activity_id, 'object_type': 'package'}
)
if not dataset or dataset['id'] != dataset_id:
raise toolkit.ObjectNotFound('Dataset not found in the activity object.')

return dataset


def get_activity_id_from_dataset_version_name(context, data_dict):
''' Returns the activity_id for the dataset version

:param dataset_id: the id of the dataset
:type dataset_id: string
:param version: the name or id of the version
:type version: string
:returns: The activity_id of the version
:rtype: string
'''
version_name, dataset_id = toolkit.get_or_bust(
data_dict,
['version', 'dataset_id']
)
version_list = dataset_version_list(context, data_dict)
for version in version_list:
if version_name in {version['name'], version['id']}:
return version['activity_id']

raise toolkit.ObjectNotFound('Version not found in the dataset.')


def dataset_has_versions(context, data_dict):
"""Check if the dataset has versions.

:param dataset_id: the id the dataset
:type dataset_id: string
:returns: True if the dataset has at least 1 version
:rtype: boolean
"""
version_list = dataset_version_list(context, data_dict)
if not version_list:
return False
return True
8 changes: 7 additions & 1 deletion ckanext/versions/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from ckanext.versions import cli, helpers
from ckanext.versions.blueprints import blueprint
from ckanext.versions.logic import action, auth
from ckanext.versions.logic import action, auth, dataset_version_action
from ckanext.versions.model import tables_exist

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -51,8 +51,13 @@ def get_actions(self):
'resource_version_current': action.resource_version_current,
'resource_version_clear': action.resource_version_clear,
'version_show': action.version_show,
'version_update': action.version_update,
'version_delete': action.version_delete,
'resource_view_list': action.resource_view_list,
'dataset_version_create': dataset_version_action.dataset_version_create,
'dataset_version_restore': dataset_version_action.dataset_version_restore,
'dataset_version_list': dataset_version_action.dataset_version_list,
'dataset_version_latest': dataset_version_action.dataset_version_latest
}

# IAuthFunctions
Expand All @@ -75,6 +80,7 @@ def get_helpers(self):
'versions_resource_version_from_activity_id': helpers.resource_version_from_activity_id,
'versions_resource_current_version': helpers.resource_current_version,
'versions_download_url': helpers.download_url,
'dataset_version_for_activity_id': helpers.dataset_version_for_activity_id,
}
return helper_functions

Expand Down
25 changes: 25 additions & 0 deletions ckanext/versions/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,33 @@
from ckan import model
from ckanext.versions.logic.dataset_version_action import dataset_version_create, dataset_version_restore


def get_context(user):
return {
'model': model,
'user': user if isinstance(user, str) else user['name']
}


def assert_version(version, checks):
assert version
for k, v in checks.items():
assert version[k] == v, "found incorrect %s of version" % k


def create_version(dataset_id, user, version_name="Default Name"):
return dataset_version_create(
get_context(user),
{
"dataset_id": dataset_id,
"name": version_name
}
)


def restore_version(dataset_id, version_id, user):
context = get_context(user)
return dataset_version_restore(context, {
'dataset_id': dataset_id,
'version_id': version_id
})
Loading