Repository: IAMconsortium/nomenclature

Issue:
Add link to project template to errors for unknown values (regions, variables, ...)
It would be good to add a link to the project template to the error message that gets displayed when a user tries to upload an unknown region, variable, scenario, ... to the Scenario Explorer.
Something along the lines of:

```console
2024-09-06 04:01:28 ERROR    The following items are not defined in the 'region' codelist:
 - VNM
Please refer to the list to the project template here: files.ece.iiasa.ac.at/{project}/{project}-template.xlsx
```

Discussion:

Patch:
diff --git a/nomenclature/__init__.py b/nomenclature/__init__.py
index 2b419af..4733c50 100644
--- a/nomenclature/__init__.py
+++ b/nomenclature/__init__.py
@@ -4,13 +4,6 @@ from pathlib import Path
 
 import yaml
 
-
-def log_error(dimension, error_list):
-    """Compile an error message and write to log"""
-    msg = f"The following {dimension}(s) are not defined in the {dimension} codelist:"
-    logging.error("\n - ".join(map(str, [msg] + error_list)))
-
-
 from nomenclature.cli import cli  # noqa
 from nomenclature.codelist import CodeList  # noqa
 from nomenclature.core import process  # noqa
diff --git a/nomenclature/codelist.py b/nomenclature/codelist.py
index 366e489..d1db586 100644
--- a/nomenclature/codelist.py
+++ b/nomenclature/codelist.py
@@ -12,10 +12,9 @@ from pydantic import BaseModel, ValidationInfo, field_validator
 from pydantic_core import PydanticCustomError
 
 import nomenclature
-from nomenclature import log_error
 from nomenclature.code import Code, MetaCode, RegionCode, VariableCode
 from nomenclature.config import CodeListConfig, NomenclatureConfig
-from nomenclature.error import ErrorCollector, custom_pydantic_errors
+from nomenclature.error import ErrorCollector, custom_pydantic_errors, log_error
 
 here = Path(__file__).parent.absolute()
 
@@ -98,9 +97,14 @@ class CodeList(BaseModel):
     def values(self):
         return self.mapping.values()
 
-    def validate_data(self, df: IamDataFrame, dimension: str) -> bool:
+    def validate_data(
+        self,
+        df: IamDataFrame,
+        dimension: str,
+        project: str | None = None,
+    ) -> bool:
         if invalid := self.validate_items(getattr(df, dimension)):
-            log_error(dimension, invalid)
+            log_error(dimension, invalid, project)
             return False
         return True
 
@@ -600,7 +604,11 @@ class VariableCodeList(CodeList):
             if self[var].agg_kwargs and not self[var].skip_region_aggregation
         ]
 
-    def validate_units(self, unit_mapping) -> bool:
+    def validate_units(
+        self,
+        unit_mapping,
+        project: None | str = None,
+    ) -> bool:
         if invalid_units := [
             (variable, unit, self.mapping[variable].unit)
             for variable, unit in unit_mapping.items()
@@ -613,14 +621,28 @@ class VariableCodeList(CodeList):
                 for v, u, e in invalid_units
             ]
             msg = "The following variable(s) are reported with the wrong unit:"
-            logging.error("\n - ".join([msg] + lst))
+            file_service_address = "https://files.ece.iiasa.ac.at"
+            logging.error(
+                "\n - ".join([msg] + lst)
+                + (
+                    f"\n\nPlease refer to {file_service_address}/{project}/"
+                    f"{project}-template.xlsx for the list of allowed units."
+                    if project is not None
+                    else ""
+                )
+            )
             return False
         return True
 
-    def validate_data(self, df: IamDataFrame, dimension: str) -> bool:
+    def validate_data(
+        self,
+        df: IamDataFrame,
+        dimension: str,
+        project: str | None = None,
+    ) -> bool:
         # validate variables
-        all_variables_valid = super().validate_data(df, dimension)
-        all_units_valid = self.validate_units(df.unit_mapping)
+        all_variables_valid = super().validate_data(df, dimension, project)
+        all_units_valid = self.validate_units(df.unit_mapping, project)
         return all_variables_valid and all_units_valid
 
     def list_missing_variables(
diff --git a/nomenclature/definition.py b/nomenclature/definition.py
index 45c2e3c..f72fa59 100644
--- a/nomenclature/definition.py
+++ b/nomenclature/definition.py
@@ -44,6 +44,7 @@ class DataStructureDefinition:
             path = Path(path)
 
         self.project_folder = path.parent
+        self.project = self.project_folder.name.split("-workflow")[0]
 
         if (file := self.project_folder / "nomenclature.yaml").exists():
             self.config = NomenclatureConfig.from_file(file=file)
@@ -98,7 +99,12 @@ class DataStructureDefinition:
         """
 
         if any(
-            getattr(self, dimension).validate_data(df, dimension) is False
+            getattr(self, dimension).validate_data(
+                df,
+                dimension,
+                self.project,
+            )
+            is False
             for dimension in (dimensions or self.dimensions)
         ):
             raise ValueError("The validation failed. Please check the log for details.")
diff --git a/nomenclature/error.py b/nomenclature/error.py
index 63926b3..3ae8750 100644
--- a/nomenclature/error.py
+++ b/nomenclature/error.py
@@ -1,3 +1,4 @@
+import logging
 import textwrap
 from collections import namedtuple
 from typing import Optional
@@ -71,3 +72,23 @@ class ErrorCollector:
 
     def __bool__(self) -> bool:
         return bool(self.errors)
+
+
+def log_error(
+    dimension: str,
+    error_list,
+    project: str | None = None,
+) -> None:
+    """Compile an error message and write to log"""
+    file_service_address = "https://files.ece.iiasa.ac.at"
+    msg = f"The following {dimension}(s) are not defined in the {dimension} codelist:"
+
+    logging.error(
+        "\n - ".join(map(str, [msg] + error_list))
+        + (
+            f"\n\nPlease refer to {file_service_address}/{project}/{project}"
+            f"-template.xlsx for the list of allowed {dimension}s."
+            if project is not None
+            else ""
+        )
+    )
diff --git a/nomenclature/processor/region.py b/nomenclature/processor/region.py
index a8888a2..b0f40a3 100644
--- a/nomenclature/processor/region.py
+++ b/nomenclature/processor/region.py
@@ -22,10 +22,9 @@ from pydantic import (
 from pydantic.types import DirectoryPath, FilePath
 from pydantic_core import PydanticCustomError
 
-from nomenclature import log_error
 from nomenclature.codelist import RegionCodeList, VariableCodeList
 from nomenclature.definition import DataStructureDefinition
-from nomenclature.error import custom_pydantic_errors, ErrorCollector
+from nomenclature.error import custom_pydantic_errors, ErrorCollector, log_error
 from nomenclature.processor import Processor
 from nomenclature.processor.utils import get_relative_path
 


Test patch:
diff --git a/tests/test_validation.py b/tests/test_validation.py
index 5ee9903..a06f27f 100644
--- a/tests/test_validation.py
+++ b/tests/test_validation.py
@@ -28,28 +28,40 @@ def test_validation_brackets(extras_definition, simple_df):
     extras_definition.validate(simple_df)
 
 
-def test_validation_fails_variable(simple_definition, simple_df):
+def test_validation_fails_variable(simple_definition, simple_df, caplog):
     """Changing a variable name raises"""
     simple_df.rename(variable={"Primary Energy": "foo"}, inplace=True)
 
     with pytest.raises(ValueError, match=MATCH_FAIL_VALIDATION):
         simple_definition.validate(simple_df)
+    assert (
+        "Please refer to https://files.ece.iiasa.ac.at/data/data-template.xlsx"
+        " for the list of allowed variables." in caplog.text
+    )
 
 
-def test_validation_fails_unit(simple_definition, simple_df):
+def test_validation_fails_unit(simple_definition, simple_df, caplog):
     """Changing a unit raises"""
     simple_df.rename(unit={"EJ/yr": "GWh/yr"}, inplace=True)
 
     with pytest.raises(ValueError, match=MATCH_FAIL_VALIDATION):
         simple_definition.validate(simple_df)
+    assert (
+        "Please refer to https://files.ece.iiasa.ac.at/data/data-template.xlsx"
+        " for the list of allowed units." in caplog.text
+    )
 
 
-def test_validation_fails_region(simple_definition, simple_df):
+def test_validation_fails_region(simple_definition, simple_df, caplog):
     """Changing a region name raises"""
     simple_df.rename(region={"World": "foo"}, inplace=True)
 
     with pytest.raises(ValueError, match=MATCH_FAIL_VALIDATION):
         simple_definition.validate(simple_df)
+    assert (
+        "Please refer to https://files.ece.iiasa.ac.at/data/data-template.xlsx"
+        " for the list of allowed regions." in caplog.text
+    )
 
 
 def test_validation_fails_region_as_int(simple_definition, simple_df):