diff --git a/ckanext/relationship/logic/action.py b/ckanext/relationship/logic/action.py index 513212a..005d699 100644 --- a/ckanext/relationship/logic/action.py +++ b/ckanext/relationship/logic/action.py @@ -44,6 +44,7 @@ def relationship_relation_create( subject_id = data_dict["subject_id"] object_id = data_dict["object_id"] relation_type = data_dict.get("relation_type") + extras = data_dict.get("extras", {}) if Relationship.by_object_id(subject_id, object_id, relation_type): return [] @@ -52,12 +53,14 @@ def relationship_relation_create( subject_id=subject_id, object_id=object_id, relation_type=relation_type, + extras=extras, ) reverse_relation = Relationship( subject_id=object_id, object_id=subject_id, relation_type=Relationship.reverse_relation_type[relation_type], + extras=extras, ) context["session"].add(relation) diff --git a/ckanext/relationship/logic/schema.py b/ckanext/relationship/logic/schema.py index 2ab7f88..52944da 100644 --- a/ckanext/relationship/logic/schema.py +++ b/ckanext/relationship/logic/schema.py @@ -1,9 +1,15 @@ from ckan.logic.schema import validator_args -from ckan.types import Schema, Validator +from ckan.types import Schema, Validator, ValidatorFactory @validator_args -def relation_create(not_empty: Validator, one_of: Validator) -> Schema: +def relation_create( + not_empty: Validator, + one_of: Validator, + default: ValidatorFactory, + convert_to_json_if_string: Validator, + dict_only: Validator, +) -> Schema: return { "subject_id": [ not_empty, @@ -14,6 +20,7 @@ def relation_create(not_empty: Validator, one_of: Validator) -> Schema: "relation_type": [ one_of(["related_to", "child_of", "parent_of"]), ], + "extras": [default("{}"), convert_to_json_if_string, dict_only], } diff --git a/ckanext/relationship/migration/relationship/versions/dd010e8e0680_add_extras_and_created_at_columns.py b/ckanext/relationship/migration/relationship/versions/dd010e8e0680_add_extras_and_created_at_columns.py new file mode 100644 index 0000000..4816514 --- /dev/null +++ b/ckanext/relationship/migration/relationship/versions/dd010e8e0680_add_extras_and_created_at_columns.py @@ -0,0 +1,35 @@ +"""Add extras and created_at columns. + +Revision ID: dd010e8e0680 +Revises: aca2ff1d3ce4 +Create Date: 2024-10-31 01:30:49.251175 + +""" + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects.postgresql import JSONB + +# revision identifiers, used by Alembic. +revision = "dd010e8e0680" +down_revision = "aca2ff1d3ce4" +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column( + "relationship_relationship", + sa.Column("extras", JSONB, nullable=False, server_default="{}"), + ) + op.add_column( + "relationship_relationship", + sa.Column( + "created_at", sa.DateTime, nullable=False, server_default=sa.text("now()") + ), + ) + + +def downgrade(): + op.drop_column("relationship_relationship", "created_at") + op.drop_column("relationship_relationship", "extras") diff --git a/ckanext/relationship/model/relationship.py b/ckanext/relationship/model/relationship.py index 02406df..69169ee 100644 --- a/ckanext/relationship/model/relationship.py +++ b/ckanext/relationship/model/relationship.py @@ -1,6 +1,9 @@ from __future__ import annotations -from sqlalchemy import Column, Text, or_ +from datetime import datetime + +from sqlalchemy import Column, DateTime, Text, or_ +from sqlalchemy.dialects.postgresql import JSONB from ckan import logic, model from ckan.model.types import make_uuid @@ -14,6 +17,8 @@ class Relationship(Base): subject_id: str = Column(Text, nullable=False) object_id: str = Column(Text, nullable=False) relation_type: str = Column(Text, nullable=False) + created_at: datetime = Column(DateTime, nullable=False, default=datetime.utcnow) + extras: dict = Column(JSONB, nullable=False, default=dict) reverse_relation_type = { "related_to": "related_to", @@ -27,7 +32,9 @@ def __repr__(self): f"id={self.id!r}, " f"subject_id={self.subject_id!r}, " f"object_id={self.object_id!r}, " - f"relation_type={self.relation_type!r})" + f"relation_type={self.relation_type!r}, " + f"created_at={self.created_at!r}, " + f"extras={self.extras!r})" ) def as_dict(self): @@ -36,6 +43,8 @@ def as_dict(self): "subject_id": self.subject_id, "object_id": self.object_id, "relation_type": self.relation_type, + "created_at": self.created_at.isoformat() if self.created_at else None, + "extras": self.extras, } @classmethod diff --git a/ckanext/relationship/tests/logic/test_action.py b/ckanext/relationship/tests/logic/test_action.py index 34ed6ac..645019c 100644 --- a/ckanext/relationship/tests/logic/test_action.py +++ b/ckanext/relationship/tests/logic/test_action.py @@ -246,6 +246,46 @@ def test_no_relation_type(self): object_id=object_id, ) + def test_created_at(self): + subject_dataset = factories.Dataset() + object_dataset = factories.Dataset() + + subject_id = subject_dataset["id"] + object_id = object_dataset["id"] + relation_type = "related_to" + + result = call_action( + "relationship_relation_create", + {"ignore_auth": True}, + subject_id=subject_id, + object_id=object_id, + relation_type=relation_type, + ) + + assert "created_at" in result[0] + assert "created_at" in result[1] + + def test_extras(self): + subject_dataset = factories.Dataset() + object_dataset = factories.Dataset() + + subject_id = subject_dataset["id"] + object_id = object_dataset["id"] + relation_type = "related_to" + extras = {"key": "value"} + + result = call_action( + "relationship_relation_create", + {"ignore_auth": True}, + subject_id=subject_id, + object_id=object_id, + relation_type=relation_type, + extras=extras, + ) + + assert result[0]["extras"] == extras + assert result[1]["extras"] == extras + @pytest.mark.usefixtures("clean_db") class TestRelationDelete: diff --git a/ckanext/relationship/tests/package_with_relationship.yaml b/ckanext/relationship/tests/package_with_relationship.yaml index 64dc5a3..df2262e 100644 --- a/ckanext/relationship/tests/package_with_relationship.yaml +++ b/ckanext/relationship/tests/package_with_relationship.yaml @@ -1,14 +1,28 @@ scheming_version: 1 -dataset_type: package_with_relationship +dataset_type: package-with-relationship about_url: http://github.com/ckan/ckanext-relationship dataset_fields: + - field_name: title + label: Title + preset: title + + - field_name: name + label: URL + preset: dataset_slug + + - field_name: owner_org + label: Organization + preset: dataset_organization + - field_name: related_packages + preset: related_entity + label: Related Packages validators: relationship_related_entity current_entity: package - current_entity_type: package_with_relationship + current_entity_type: package-with-relationship related_entity: package - related_entity_type: package_with_relationship + related_entity_type: package-with-relationship relation_type: related_to resource_fields: diff --git a/test.ini b/test.ini index 3c24f68..1f611f7 100644 --- a/test.ini +++ b/test.ini @@ -15,6 +15,10 @@ ckan.plugins = scheming_datasets scheming.dataset_schemas = ckanext.relationship.tests:package_with_relationship.yaml +scheming.presets = + ckanext.relationship:presets.yaml + ckanext.scheming:presets.json + # Logging configuration [loggers] keys = root, ckan, sqlalchemy