diff --git a/rdmo/conditions/renderers/__init__.py b/rdmo/conditions/renderers/__init__.py
index e597e65fb9..3e2dfd4e7e 100644
--- a/rdmo/conditions/renderers/__init__.py
+++ b/rdmo/conditions/renderers/__init__.py
@@ -11,6 +11,7 @@ def render_document(self, xml, conditions):
xml.startElement('rdmo', {
'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
'version': self.version,
+ 'required': self.required,
'created': self.created
})
for condition in conditions:
diff --git a/rdmo/core/renderers.py b/rdmo/core/renderers.py
index 91e7e8417b..9d64d1175c 100644
--- a/rdmo/core/renderers.py
+++ b/rdmo/core/renderers.py
@@ -1,6 +1,7 @@
import re
from io import StringIO
+from django.conf import settings
from django.utils.encoding import smart_str
from django.utils.timezone import get_current_timezone, now
from django.utils.xmlutils import SimplerXMLGenerator
@@ -51,6 +52,10 @@ def render_document(self, xml, data):
def version(self):
return __version__
+ @property
+ def required(self):
+ return settings.EXPORT_MIN_REQUIRED_VERSION
+
@property
def created(self):
return now().astimezone(get_current_timezone()).isoformat()
diff --git a/rdmo/core/settings.py b/rdmo/core/settings.py
index c6610be4de..29f701ee46 100644
--- a/rdmo/core/settings.py
+++ b/rdmo/core/settings.py
@@ -295,6 +295,8 @@
EXPORT_CONTENT_DISPOSITION = 'attachment'
+EXPORT_MIN_REQUIRED_VERSION = '2.1.0'
+
PROJECT_TABLE_PAGE_SIZE = 20
PROJECT_VISIBILITY = True
diff --git a/rdmo/core/tests/test_renderers.py b/rdmo/core/tests/test_renderers.py
index a57b3417ea..8782a7b3a4 100644
--- a/rdmo/core/tests/test_renderers.py
+++ b/rdmo/core/tests/test_renderers.py
@@ -1,3 +1,7 @@
+from django.conf import settings
+
+from rdmo import __version__
+
from ..renderers import BaseXMLRenderer
@@ -7,6 +11,7 @@ def render_document(self, xml, data):
xml.startElement('rdmo', {
'xmlns:dc': "http://purl.org/dc/elements/1.1/",
'version': self.version,
+ 'required': self.required,
'created': self.created
})
self.render_text_element(xml, 'text', {}, data['text'])
@@ -17,6 +22,8 @@ def test_render():
renderer = TestRenderer()
xml = renderer.render({'text': 'test'})
assert 'test' in xml
+ assert f'version="{__version__}"' in xml
+ assert f'required="{settings.EXPORT_MIN_REQUIRED_VERSION}"' in xml
def test_render_ascii_code():
diff --git a/rdmo/core/xml.py b/rdmo/core/xml.py
index 264977352e..5d1cd10894 100644
--- a/rdmo/core/xml.py
+++ b/rdmo/core/xml.py
@@ -45,17 +45,49 @@ def validate_root(root: Optional[xmlElement]) -> Tuple[bool, Optional[str]]:
def validate_and_get_xml_version_from_root(root: xmlElement) -> Tuple[Optional[Version], list]:
- unparsed_root_version = root.attrib.get('version') or LEGACY_RDMO_XML_VERSION
- root_version, rdmo_version = parse(unparsed_root_version), parse(RDMO_INSTANCE_VERSION)
- if root_version > rdmo_version:
- logger.info('Import failed version validation (%s > %s)', root_version, rdmo_version)
+ rdmo_version = parse(RDMO_INSTANCE_VERSION)
+
+ # Extract version attributes from the XML root
+ unparsed_required_version = root.attrib.get('required') # New required version field
+ unparsed_root_version = root.attrib.get('version') or LEGACY_RDMO_XML_VERSION # Fallback to legacy default
+
+ # Validate the 'required' attribute if it exists
+ if unparsed_required_version:
+ try:
+ required_version = parse(unparsed_required_version)
+ except ValueError:
+ logger.info('Import failed: Invalid "required" format in XML (%s)', unparsed_required_version)
+ errors = [_('The "required" attribute in this RDMO XML file is not a valid version.')]
+ return None, errors
+
+ if required_version > rdmo_version:
+ logger.info('Import failed: Required version (%s) > RDMO instance version (%s)', required_version,
+ rdmo_version)
+ errors = [
+ _('This RDMO XML file requires a newer RDMO version to be imported.'),
+ f'Required version: {required_version}, Current version: {rdmo_version}.'
+ ]
+ return None, errors
+
+ # Fallback to validate the legacy 'version' field
+ try:
+ xml_version = parse(unparsed_root_version)
+
+ except ValueError:
+ logger.info('Import failed: Invalid "version" format in XML (%s)', unparsed_root_version)
+ errors = [_('The "version" attribute in this RDMO XML file is not a valid version.')]
+ return None, errors
+
+ # RDMO 1.x can not import XMLs from RDMO 2.x
+ if rdmo_version < parse('2.0.0') and xml_version >= parse('2.0.0'):
+ logger.info('Import failed: Backwards compatibility is not supported for RDMO versions < 2.0.0')
errors = [
- _('This RDMO XML file does not have a valid version number.'),
- f'XML Version ({root_version}) is greater than RDMO instance version {rdmo_version}'
+ _('This RDMO XML file was created with a newer version of RDMO and cannot be imported.'),
+ _('Backwards compatibility is not supported for RDMO versions lower than 2.0.0.')
]
return None, errors
- return root_version, []
+ return xml_version, []
def validate_legacy_elements(elements: dict, root_version: Version) -> list:
@@ -117,13 +149,13 @@ def parse_xml_to_elements(xml_file=None) -> Tuple[OrderedDict, list]:
return OrderedDict(), errors
# step 3.1.1: validate the legacy elements
- legacy_errors = validate_legacy_elements(elements, parse(root.attrib.get('version', LEGACY_RDMO_XML_VERSION)))
+ legacy_errors = validate_legacy_elements(elements, root_version)
if legacy_errors:
errors.extend(legacy_errors)
return OrderedDict(), errors
# step 4: convert elements from previous versions
- elements = convert_elements(elements, parse(root.attrib.get('version', LEGACY_RDMO_XML_VERSION)))
+ elements = convert_elements(elements, root_version)
# step 5: order the elements and return
# ordering of elements is done in the import_elements function
@@ -232,9 +264,6 @@ def strip_ns(tag, ns_map):
def convert_elements(elements, version: Version):
- if not isinstance(version, Version):
- raise TypeError('Version should be of parsed version type.')
-
if version < parse('2.0.0'):
validate_pre_conversion_for_missing_key_in_legacy_elements(elements, version)
elements = convert_legacy_elements(elements)
diff --git a/rdmo/domain/renderers/__init__.py b/rdmo/domain/renderers/__init__.py
index fce1febd34..91580f4d90 100644
--- a/rdmo/domain/renderers/__init__.py
+++ b/rdmo/domain/renderers/__init__.py
@@ -9,6 +9,7 @@ def render_document(self, xml, attributes):
xml.startElement('rdmo', {
'xmlns:dc': "http://purl.org/dc/elements/1.1/",
'version': self.version,
+ 'required': self.required,
'created': self.created
})
for attribute in attributes:
diff --git a/rdmo/management/tests/helpers_xml.py b/rdmo/management/tests/helpers_xml.py
index d5fd64a420..97b719baf0 100644
--- a/rdmo/management/tests/helpers_xml.py
+++ b/rdmo/management/tests/helpers_xml.py
@@ -1,25 +1,30 @@
+from packaging.version import parse
+from rdmo import __version__
from rdmo.core.xml import parse_xml_to_elements, read_xml, resolve_file
+current_version = parse(__version__)
+
xml_test_files = {
"xml/elements/catalogs.xml":
None,
"xml/elements/updated-and-changed/optionsets-1.xml":
None,
'file-does-not-exist.xml':
- 'This file does not exists',
+ 'This field may not be blank.', # or 'This file does not exists'
"xml/error.xml":
"The content of the XML file does not consist of well-formed data or markup. XML Parsing Error: syntax error: line 1, column 0", # noqa: E501
"xml/project.xml":
"This XML does not contain RDMO content.",
'xml/error-version.xml':
- 'This RDMO XML file does not have a valid version number. XML Version (99.9.9) is greater',
+ 'The "version" attribute in this RDMO XML file is not a valid version.',
+ 'xml/error-version-required.xml':
+ f'This RDMO XML file requires a newer RDMO version to be imported. Required version: 99.9.9, Current version: {current_version}', # noqa: E501
'xml/elements/legacy/catalog-error-key.xml':
'XML Parsing Error: Missing legacy elements',
}
xml_error_files = {k: v for k,v in xml_test_files.items() if v is not None}
-xml_error_files['file-does-not-exist.xml'] = 'This field may not be blank.'
def read_xml_and_parse_to_root_and_elements(file):
errors = []
diff --git a/rdmo/management/tests/test_commands.py b/rdmo/management/tests/test_commands.py
index e3263b1925..661ac47571 100644
--- a/rdmo/management/tests/test_commands.py
+++ b/rdmo/management/tests/test_commands.py
@@ -23,4 +23,6 @@ def test_import(db, settings, xml_file_path, error_message):
with pytest.raises(CommandError) as e:
call_command('import', xml_file, stdout=stdout, stderr=stderr)
+ if error_message == 'This field may not be blank.':
+ error_message = 'This file does not exists' # overwrite error message for cli import
assert str(e.value).startswith(error_message)
diff --git a/rdmo/options/renderers/__init__.py b/rdmo/options/renderers/__init__.py
index 41e18c2e63..f7342577bd 100644
--- a/rdmo/options/renderers/__init__.py
+++ b/rdmo/options/renderers/__init__.py
@@ -12,6 +12,7 @@ def render_document(self, xml, optionsets):
xml.startElement('rdmo', {
'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
'version': self.version,
+ 'required': self.required,
'created': self.created
})
for optionset in optionsets:
@@ -25,6 +26,7 @@ def render_document(self, xml, options):
xml.startElement('rdmo', {
'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
'version': self.version,
+ 'required': self.required,
'created': self.created
})
for option in options:
diff --git a/rdmo/projects/renderers.py b/rdmo/projects/renderers.py
index 2a3e543418..75d18a1442 100644
--- a/rdmo/projects/renderers.py
+++ b/rdmo/projects/renderers.py
@@ -7,6 +7,7 @@ def render_document(self, xml, project):
xml.startElement('project', {
'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
'version': self.version,
+ 'required': self.required,
'created': self.created
})
self.render_text_element(xml, 'title', {}, project['title'])
diff --git a/rdmo/questions/renderers/__init__.py b/rdmo/questions/renderers/__init__.py
index 112ee856be..81e6753a0f 100644
--- a/rdmo/questions/renderers/__init__.py
+++ b/rdmo/questions/renderers/__init__.py
@@ -20,6 +20,7 @@ def render_document(self, xml, catalogs):
xml.startElement('rdmo', {
'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
'version': self.version,
+ 'required': self.required,
'created': self.created
})
for catalog in catalogs:
@@ -35,6 +36,7 @@ def render_document(self, xml, sections):
xml.startElement('rdmo', {
'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
'version': self.version,
+ 'required': self.required,
'created': self.created
})
for section in sections:
@@ -49,6 +51,7 @@ def render_document(self, xml, pages):
xml.startElement('rdmo', {
'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
'version': self.version,
+ 'required': self.required,
'created': self.created
})
for page in pages:
@@ -63,6 +66,7 @@ def render_document(self, xml, questionsets):
xml.startElement('rdmo', {
'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
'version': self.version,
+ 'required': self.required,
'created': self.created
})
for questionset in questionsets:
@@ -77,6 +81,7 @@ def render_document(self, xml, questions):
xml.startElement('rdmo', {
'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
'version': self.version,
+ 'required': self.required,
'created': self.created
})
for question in questions:
diff --git a/rdmo/tasks/renderers/__init__.py b/rdmo/tasks/renderers/__init__.py
index 4257b7e3f2..7244d57f57 100644
--- a/rdmo/tasks/renderers/__init__.py
+++ b/rdmo/tasks/renderers/__init__.py
@@ -13,6 +13,7 @@ def render_document(self, xml, tasks):
xml.startElement('rdmo', {
'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
'version': self.version,
+ 'required': self.required,
'created': self.created
})
for task in tasks:
diff --git a/rdmo/views/renderers/__init__.py b/rdmo/views/renderers/__init__.py
index 02ee2c3ebe..fb7cebec49 100644
--- a/rdmo/views/renderers/__init__.py
+++ b/rdmo/views/renderers/__init__.py
@@ -9,6 +9,7 @@ def render_document(self, xml, views):
xml.startElement('rdmo', {
'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
'version': self.version,
+ 'required': self.required,
'created': self.created
})
for view in views:
diff --git a/testing/xml/error-version-required.xml b/testing/xml/error-version-required.xml
new file mode 100644
index 0000000000..7c4347d3d2
--- /dev/null
+++ b/testing/xml/error-version-required.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/testing/xml/error-version.xml b/testing/xml/error-version.xml
index efaaccaf70..2bdff9578c 100644
--- a/testing/xml/error-version.xml
+++ b/testing/xml/error-version.xml
@@ -1,3 +1,3 @@
-
+