Repository: ckan/ckan

Issue:
Validation doesn't work on plugins inheriting the core `IDatasetForm` methods
**CKAN version**
Current master (2.10)

**Describe the bug**
Consider the following test:

```python
import ckan.plugins as plugins
import ckan.plugins.toolkit as tk

class CustomDatasetType(plugins.SingletonPlugin, tk.DefaultDatasetForm):

    plugins.implements(plugins.IDatasetForm, inherit=True)

    def package_types(self):

        return ["custom_dataset"]


@pytest.mark.ckan_config("ckan.auth.create_unowned_dataset", True)
def test_search_datasets_returns_only_datasts(self, app):
 
    factories.Dataset(type="custom_dataset")

```

This will fail with an SQLAlchemy error

```
sqlalchemy.exc.IntegrityError: (psycopg2.errors.NotNullViolation) null value in column "name" violates not-null constraint

```

Upon further investigation, when calling the NAVL validation function from `package_create`:

https://github.com/ckan/ckan/blob/a18603ec182e279488c0e91b78cd957a3ed49d30/ckan/logic/action/create.py#L178-L179

`data` is an empty dict!

I'll send a proposed fix as PR


Discussion:

Resolving patch:
diff --git a/ckan/plugins/interfaces.py b/ckan/plugins/interfaces.py
--- a/ckan/plugins/interfaces.py
+++ b/ckan/plugins/interfaces.py
@@ -1267,8 +1267,9 @@ def resource_form(self, package_type: str) -> str:
         '''
         return ''
 
-    def validate(self, context: Context, data_dict: DataDict, schema: Schema,
-                 action: str) -> tuple[dict[str, Any], dict[str, Any]]:
+    def validate(
+            self, context: Context, data_dict: DataDict, schema: Schema,
+            action: str) -> Optional[tuple[dict[str, Any], dict[str, Any]]]:
         u'''Customize validation of datasets.
 
         When this method is implemented it is used to perform all validation
@@ -1298,7 +1299,7 @@ def validate(self, context: Context, data_dict: DataDict, schema: Schema,
           and lists-of-string-error-messages as values
         :rtype: (dictionary, dictionary)
         '''
-        return {}, {}
+        return
 
     def prepare_dataset_blueprint(self, package_type: str,
                                   blueprint: Blueprint) -> Blueprint:
@@ -1483,8 +1484,9 @@ def setup_template_variables(self, context: Context,
         Add variables to c just prior to the template being rendered.
         '''
 
-    def validate(self, context: Context, data_dict: DataDict, schema: Schema,
-                 action: str) -> tuple[dict[str, Any], dict[str, Any]]:
+    def validate(
+            self, context: Context, data_dict: DataDict, schema: Schema,
+            action: str) -> Optional[tuple[dict[str, Any], dict[str, Any]]]:
         u'''Customize validation of groups.
 
         When this method is implemented it is used to perform all validation
@@ -1515,7 +1517,7 @@ def validate(self, context: Context, data_dict: DataDict, schema: Schema,
           and lists-of-string-error-messages as values
         :rtype: (dictionary, dictionary)
         '''
-        return {}, {}
+        return
 
     def prepare_group_blueprint(self, group_type: str,
                                 blueprint: Blueprint) -> Blueprint:
diff --git a/ckanext/example_idatasetform/plugin.py b/ckanext/example_idatasetform/plugin.py
--- a/ckanext/example_idatasetform/plugin.py
+++ b/ckanext/example_idatasetform/plugin.py
@@ -190,3 +190,15 @@ def package_form(self) -> Any:
     # legacy support for the deprecated method works.
     def check_data_dict(self, data_dict: dict[str, Any], schema: Any = None):
         ExampleIDatasetFormPlugin.num_times_check_data_dict_called += 1
+
+
+class ExampleIDatasetFormInheritPlugin(plugins.SingletonPlugin,
+        tk.DefaultDatasetForm):
+    """An example IDatasetForm CKAN plugin, inheriting all methods
+    from the default interface.
+    """
+    plugins.implements(plugins.IDatasetForm, inherit=True)
+
+    def package_types(self):
+
+        return ["custom_dataset"]


Test patch:
diff --git a/ckan/tests/logic/action/test_create.py b/ckan/tests/logic/action/test_create.py
--- a/ckan/tests/logic/action/test_create.py
+++ b/ckan/tests/logic/action/test_create.py
@@ -666,6 +666,7 @@ def test_resource_create_for_update(self):
             helpers.call_action('resource_create', package_id=dataset['id'], url='http://example.com', description='hey')
             assert mock_package_show.call_args_list[0][0][0].get('for_update') is True
 
+
 @pytest.mark.usefixtures("non_clean_db")
 class TestMemberCreate(object):
     def test_group_member_creation(self):
diff --git a/ckanext/example_idatasetform/tests/test_example_idatasetform.py b/ckanext/example_idatasetform/tests/test_example_idatasetform.py
--- a/ckanext/example_idatasetform/tests/test_example_idatasetform.py
+++ b/ckanext/example_idatasetform/tests/test_example_idatasetform.py
@@ -526,3 +526,12 @@ def test_template_with_options(self, type_, app):
         url = url_for(type_ + '.read', id=dataset['name'])
         resp = app.get(url, status=200)
         assert resp.body == 'Hello, {}!'.format(type_)
+
+
+@pytest.mark.ckan_config("ckan.plugins", u"example_idatasetform_inherit")
+@pytest.mark.usefixtures("with_plugins")
+def test_validation_works_on_default_validate():
+
+    dataset = factories.Dataset(name="my_dataset", type="custom_dataset")
+
+    assert dataset["name"] == "my_dataset"