Repository: astropy/astropy

Issue:
VOTable writer does not write out GROUPs within RESOURCEs.
### Description

When writing out a VOTable from `VOTableFile` object, any `Group` children of `Resource` objects are not written out.

`VOTableFile`, `TableElement` and `Group` objects can also have children of type `Group`, and in all those cases the `Group` children are properly written out.


### Expected behavior

If I read in a VOTable containing a `RESOURCE` element that has a child `GROUP` element, or I construct such a `VOTableFile` internally using the `astropy.io.votable` API, then I write out the VOTable to xml, I expect the `GROUP` child of the `RESOURCE` to be included in the output.

### How to Reproduce

This script reads in `test_in.xml` which has `GROUP` children for `VOTABLE`, `GROUP`, `RESOURCE` and `TABLE`.

Then it prints the contents of the `GROUP` under the `RESOURCE` to show that it has not been lost on read.

Then it writes the VOTable to `test_out.xml`.  Note that the `GROUP` under the `RESOURCE` is missing.

```python
from astropy.io.votable import parse

votable = parse("test_in.xml")
print(f"{votable.resources[0].groups[0]=}")
votable.to_xml("test_out.xml")
```

Output:
```
votable.resources[0].groups[0]=<GROUP>... 1 entries ...</GROUP>
```

test_in.xml:
```xml
<?xml version="1.0" encoding="utf-8"?>
<VOTABLE version="1.4" xmlns="http://www.ivoa.net/xml/VOTable/v1.3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.ivoa.net/xml/VOTable/v1.3 http://www.ivoa.net/xml/VOTable/VOTable-1.4.xsd">
     <GROUP ID="VOTABLE_child">
          <PARAM arraysize="*" datatype="char" name="VOTABLE_group_param" value=""/>
               <GROUP ID="GROUP_child">
                    <PARAM arraysize="*" datatype="char" name="GROUP_group_param" value=""/>
               </GROUP>
     </GROUP>
     <RESOURCE ID="soda-service" type="meta" utype="adhoc:service">
          <PARAM ID="standardID" arraysize="*" datatype="char" name="standardID" value="ivo://ivoa.net/std/SODAnext#sync-1.0"/>
          <PARAM ID="accessURL" arraysize="*" datatype="char" name="accessURL" value="https://abc.net"/>
          <GROUP ID="RESOURCE_child">
               <PARAM arraysize="*" datatype="char" name="RESOURCE_group_param" value=""/>
          </GROUP>
          <TABLE>
               <GROUP ID="TABLE_child">
                    <PARAM arraysize="*" datatype="char" name="TABLE_group_param" value=""/>
               </GROUP>
          </TABLE>
     </RESOURCE>
</VOTABLE>
```

test_out.xml:
```xml
<?xml version="1.0" encoding="utf-8"?>
<!-- Produced with astropy.io.votable version 6.1.0
     http://www.astropy.org/ -->
<VOTABLE version="1.4" xmlns="http://www.ivoa.net/xml/VOTable/v1.3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.ivoa.net/xml/VOTable/v1.3 http://www.ivoa.net/xml/VOTable/VOTable-1.4.xsd">
 <GROUP ID="VOTABLE_child">
  <PARAM ID="VOTABLE_group_param" arraysize="*" datatype="char" name="VOTABLE_group_param" value=""/>
  <GROUP ID="GROUP_child">
   <PARAM ID="GROUP_group_param" arraysize="*" datatype="char" name="GROUP_group_param" value=""/>
  </GROUP>
 </GROUP>
 <RESOURCE ID="soda-service" type="meta" utype="adhoc:service">
  
  <PARAM ID="standardID" arraysize="*" datatype="char" name="standardID" value="ivo://ivoa.net/std/SODAnext#sync-1.0"/>
  <PARAM ID="accessURL" arraysize="*" datatype="char" name="accessURL" value="https://abc.net"/>
  <TABLE>
   <GROUP ID="TABLE_child">
    <PARAM ID="TABLE_group_param" arraysize="*" datatype="char" name="TABLE_group_param" value=""/>
   </GROUP>
  </TABLE>
 </RESOURCE>
</VOTABLE>
```


### Versions

```python
import astropy
try:
    astropy.system_info()
except AttributeError:
    import platform; print(platform.platform())
    import sys; print("Python", sys.version)
    import astropy; print("astropy", astropy.__version__)
    import numpy; print("Numpy", numpy.__version__)
    import erfa; print("pyerfa", erfa.__version__)
    try:
        import scipy
        print("Scipy", scipy.__version__)
    except ImportError:
        print("Scipy not installed")
    try:
        import matplotlib
        print("Matplotlib", matplotlib.__version__)
    except ImportError:
        print("Matplotlib not installed")
```
```
macOS-14.4.1-arm64-arm-64bit
Python 3.10.14 (main, May  6 2024, 14:42:37) [Clang 14.0.6 ]
astropy 6.1.0
Numpy 1.26.4
pyerfa 2.0.1.4
Scipy not installed
Matplotlib not installed
```


Discussion:

Patch:
diff --git a/astropy/io/votable/tree.py b/astropy/io/votable/tree.py
index f4678798f9..953cde027f 100644
--- a/astropy/io/votable/tree.py
+++ b/astropy/io/votable/tree.py
@@ -3969,13 +3969,16 @@ class Resource(
                 w.element("DESCRIPTION", self.description, wrap=True)
             if self.mivot_block is not None and self.type == "meta":
                 self.mivot_block.to_xml(w)
-            for element_set in (
+            element_sets = [
                 self.coordinate_systems,
                 self.time_systems,
                 self.params,
                 self.infos,
                 self.links,
-            ):
+            ]
+            if kwargs["version_1_2_or_later"]:
+                element_sets.append(self.groups)
+            for element_set in element_sets:
                 for element in element_set:
                     element.to_xml(w, **kwargs)
 
diff --git a/docs/changes/io.votable/17344.bugfix.rst b/docs/changes/io.votable/17344.bugfix.rst
new file mode 100644
index 0000000000..73f65d8b75
--- /dev/null
+++ b/docs/changes/io.votable/17344.bugfix.rst
@@ -0,0 +1,1 @@
+Updated xml writer for VOTable Resource elements to include groups.


Test patch:
diff --git a/astropy/io/votable/tests/test_resource.py b/astropy/io/votable/tests/test_resource.py
index 92f4cf0ff5..ded738e4c8 100644
--- a/astropy/io/votable/tests/test_resource.py
+++ b/astropy/io/votable/tests/test_resource.py
@@ -1,5 +1,7 @@
 # Licensed under a 3-clause BSD style license - see LICENSE.rst
 # LOCAL
+import io
+
 from astropy.io.votable import parse
 from astropy.utils.data import get_pkg_data_filename
 
@@ -20,3 +22,28 @@ def test_resource_groups():
     assert len(params) == 2
     assert params[0].name == "standardID"
     assert params[1].name == "accessURL"
+
+
+def test_roundtrip():
+    # Issue #16511 VOTable writer does not write out GROUPs within RESOURCEs
+
+    # Read the VOTABLE
+    votable = parse(get_pkg_data_filename("data/resource_groups.xml"))
+
+    bio = io.BytesIO()
+    votable.to_xml(bio)
+    bio.seek(0)
+    votable = parse(bio)
+
+    resource = votable.resources[0]
+    groups = resource.groups
+    params = resource.params
+
+    # Test that params inside groups are not outside
+
+    assert len(groups[0].entries) == 1
+    assert groups[0].entries[0].name == "ID"
+
+    assert len(params) == 2
+    assert params[0].name == "standardID"
+    assert params[1].name == "accessURL"