Repository: hydroshare/hydroshare

Issue:
Adding a resource to a collection fails
**Description of the bug**
I tried to add a resource to a collection and received the following error
![image](https://user-images.githubusercontent.com/3456833/235454226-7946b26b-456f-4c3f-82ec-0a9f2023ee82.png)


Steps to reproduce the bug:
1. Go to a collection where you have edit permissions.  I was working on https://www.hydroshare.org/resource/b6c4fcad40c64c4cb4dd7d4a25d0db6e/ 
2. Switch to edit mode
3. Click on the button to Add resources
4. Select a Resource
5. The error occurs

**Expected behavior**
The resource should be added to the collection

**Additional information**
This may be related to #5057 as I had just prior to attempting to add, removed a resource from this collection.



Discussion:
@dtarb do you remember what resource you were attempting to add to the collection when it failed? Thanks
I was trying to add this resource https://www.hydroshare.org/resource/45b43d72928048a8bc10a009d932f769/.  
Tracking some info as I take a look
```
>>> resources = BaseResource.objects.filter(collections__short_id='b6c4fcad40c64c4cb4dd7d4a25d0db6e').all()
>>> resources
<QuerySet [<BaseResource: Great Salt Lake Area Volume Data>, <BaseResource: Great Salt Lake Basin Digital Elevation Model>, <BaseResource: Great Salt Lake Bathymetry>, <BaseResource: Great Salt Lake Mineral Extraction Water Withdrawals>, <BaseResource: Great Salt Lake Basin Watershed Boundaries Shapefile>]>
>>> resources.count()
5
>>> for res in resources:
...     print(res)
...     for rel in res.metadata.relations.all():
...         print(rel)
...
Great Salt Lake Area Volume Data
source Tarboton, D. (2017). Great Salt Lake Bathymetry, HydroShare, http://www.hydroshare.org/resource/582060f00f6b443bb26e896426d9f62a
isPartOf Tarboton, D. (2023). Collection of Great Salt Lake Data, HydroShare, http://www.hydroshare.org/resource/b6c4fcad40c64c4cb4dd7d4a25d0db6e
Great Salt Lake Basin Digital Elevation Model
isPartOf Tarboton, D. (2023). Collection of Great Salt Lake Data, HydroShare, http://www.hydroshare.org/resource/b6c4fcad40c64c4cb4dd7d4a25d0db6e
Great Salt Lake Bathymetry
references Baskin, R. L., (2005), "Calculation of Area and Volume for the South Part of Great Salt Lake, Utah," Open-File Report 2005–1327, USGS, Salt Lake City, http://pubs.water.usgs.gov/ofr20051327
isReferencedBy Tarboton, D. (2017). Great Salt Lake Area Volume Data, HydroShare, http://www.hydroshare.org/resource/89125e9a3af544eab2479b7a974100ba
references Baskin, R. L., (2006), "Calculation of Area and Volume for the North Part of Great Salt Lake, Utah," Open-File Report 2006–1359, USGS, Salt Lake City, http://pubs.water.usgs.gov/ofr20061359.
isPartOf Tarboton, D. (2023). Collection of Great Salt Lake Data, HydroShare, http://www.hydroshare.org/resource/b6c4fcad40c64c4cb4dd7d4a25d0db6e
Great Salt Lake Mineral Extraction Water Withdrawals
isDescribedBy Merck, M. and D. Tarboton, (2023 submitted), "The Salinity of the Great Salt Lake and its Deep Brine Layer," Water, https://www.mdpi.com/journal/water
isPartOf Tarboton, D. (2023). Collection of Great Salt Lake Data, HydroShare, http://www.hydroshare.org/resource/b6c4fcad40c64c4cb4dd7d4a25d0db6e
Great Salt Lake Basin Watershed Boundaries Shapefile
references Mohammed, I. N. and D. G. Tarboton, (2011), "On the Interaction between Bathymetry and Climate in the System Dynamics and Preferred Levels of the Great Salt Lake," Water Resour. Res., 47: W02525, http://dx.doi.org/10.1029/2010WR009561.
references Mohammed, I. N. and D. G. Tarboton, (2012), "An examination of the sensitivity of the Great Salt Lake to changes in inputs," Water Resour. Res., 48(11): W11511, http://dx.doi.org/10.1029/2012WR011908.
isPartOf Tarboton, D. (2023). Collection of Great Salt Lake Data, HydroShare, http://www.hydroshare.org/resource/b6c4fcad40c64c4cb4dd7d4a25d0db6e
```

Looking at the collection:
```
>>> col = BaseResource.objects.filter(short_id='b6c4fcad40c64c4cb4dd7d4a25d0db6e').all()
>>> rels = col.first().metadata.relations.all()
>>> for rel in rels:
...     print(rel)
...
hasPart Tarboton, D. (2017). Great Salt Lake Area Volume Data, HydroShare, http://www.hydroshare.org/resource/89125e9a3af544eab2479b7a974100ba
hasPart Tarboton, D. (2017). Great Salt Lake Bathymetry, HydroShare, http://www.hydroshare.org/resource/582060f00f6b443bb26e896426d9f62a
hasPart Tarboton, D. (2022). Great Salt Lake Basin Digital Elevation Model, HydroShare, http://www.hydroshare.org/resource/e660640a7b084794aa2d70dc77cfa67b
hasPart Merck, M., D. Tarboton (2023). Great Salt Lake Mineral Extraction Water Withdrawals, HydroShare, http://www.hydroshare.org/resource/46b98820b7214850aadf6fa60c9bc1d1
hasPart Tarboton, D. (2023). Great Salt Lake Basin Watershed Boundaries Shapefile, HydroShare, http://www.hydroshare.org/resource/2796b46fa75140be821032846d6140c0
```
Thought maybe the issue was that the resource-to-be-added was versioned, but tested and that was not the case.
Here's something interesting
Taking a look at the resource that Dave attempted to add to the collection
```
>>> res = BaseResource.objects.filter(short_id='45b43d72928048a8bc10a009d932f769').all()
>>> rel = res.first().metadata.relations.all()
>>> for re in rel:
...     print(re)
...
isPartOf Tarboton, D. (2023). Collection of Great Salt Lake Data, HydroShare, http://www.hydroshare.org/resource/b6c4fcad40c64c4cb4dd7d4a25d0db6e
isVersionOf Tarboton, D. (2017). Great Salt Lake Level and Volume, HydroShare, http://www.hydroshare.org/resource/b26090299ec947c692d4ee4651815579
references Tarboton, D. (2017). Great Salt Lake Area Volume Data, HydroShare, http://www.hydroshare.org/resource/89125e9a3af544eab2479b7a974100ba
```
We can see the the resource that Dave tried to add
https://www.hydroshare.org/resource/45b43d72928048a8bc10a009d932f769/
"thinks" that is is already `partOf` the collection

Likely this is just because the adding failed partway through? But interesting nonetheless
@pkdash looks like you're right, this is probably a regression that causes duplicate listing of resources in our modal
Probably introduced by PR https://github.com/hydroshare/hydroshare/pull/5038 as part of work on https://github.com/hydroshare/hydroshare/issues/5030

Looks like the problem is for resources with multiple owners
https://github.com/hydroshare/hydroshare/blob/develop/hs_collection_resource/views.py#L38

Distinct()
https://docs.djangoproject.com/en/3.2/ref/models/querysets/#django.db.models.query.QuerySet.distinct
fixed in 2.5.2 hotfix
@devincowan Sadly this is not fixed for me.  
![image](https://user-images.githubusercontent.com/3456833/236205133-6f217611-ecf9-4a35-aab4-b29002515755.png)

I've added you dcowan as an editor in the collection https://www.hydroshare.org/resource/b6c4fcad40c64c4cb4dd7d4a25d0db6e/ and resource I am trying to add 
https://www.hydroshare.org/resource/45b43d72928048a8bc10a009d932f769/ in case looking at these will help.

Resolving patch:
diff --git a/hs_collection_resource/management/commands/fix_collection_version_missing_ispartof.py b/hs_collection_resource/management/commands/fix_collection_version_missing_ispartof.py
--- a/hs_collection_resource/management/commands/fix_collection_version_missing_ispartof.py
+++ b/hs_collection_resource/management/commands/fix_collection_version_missing_ispartof.py
@@ -1,6 +1,7 @@
 from django.core.management.base import BaseCommand
 from hs_core.enums import RelationTypes
 from hs_core.models import BaseResource
+from hs_core.hydroshare import current_site_url
 
 
 class Command(BaseCommand):
@@ -17,19 +18,86 @@ def add_arguments(self, parser):
 
     def handle(self, *args, **options):
         dry_run = options['dryrun']
+        errors = []
+        current_site = current_site_url()
         collections = BaseResource.objects.filter(resource_type="CollectionResource").all()
+        print('-' * 100)
+        print("1. Checking Collection's metadata...")
+        col_count = collections.count()
+        print(f"{col_count} collections to check")
+        n = 0
+        fixed = 0
         for collection in collections:
-            print(f"Checking collection: {collection.short_id}")
+            n = n + 1
+            col_citation = collection.get_citation()
             for res in collection.resources.all():
                 rel_values = [rel.value for rel in res.metadata.relations.filter(type=RelationTypes.isPartOf).all()]
-                print(f"Relations: {rel_values}")
-                if collection.get_citation() not in rel_values:
-                    print("IsPartOf relation missing from resource {res}.")
+                if col_citation not in rel_values:
+                    print(f"{n}/{col_count}:For collection:{collection}, {current_site}/resource/{collection.short_id}")
+                    print(f"IsPartOf relation missing from {res}, {current_site}/resource/{res.short_id}")
                     if dry_run:
                         print("SKIPPING creating IsPartOf relation because dry_run")
                     else:
                         print("Creating IsPartOf relation...")
                         res.metadata.create_element('relation',
                                                     type=RelationTypes.isPartOf,
-                                                    value=collection.get_citation())
+                                                    value=col_citation)
+                    fixed = fixed + 1
+                res_citation = res.get_citation()
+                if not collection.metadata.relations.filter(type=RelationTypes.hasPart, value=res_citation).exists():
+                    print(f"hasPart relation missing from collection: "
+                          f"{collection}, {current_site}/resource/{collection.short_id}")
+                    if dry_run:
+                        print("SKIPPING creating hasPart relation because dry_run")
+                    else:
+                        collection.metadata.create_element('relation',
+                                                           type=RelationTypes.hasPart,
+                                                           value=res_citation)
+                    fixed = fixed + 1
+        print(f"{fixed} metadata relations were missing")
+
+        print('-' * 100)
+        print("2. Remove dangling isPart metadata from resources that are not included in collections")
+        resources = BaseResource.objects.all()
+        res_count = resources.count()
+        print(f"{res_count} resources to check")
+        i = 0
+        fixed = 0
+        for res in resources:
+            i = i + 1
+            isPartOf_relations = None
+            if res.metadata:
+                isPartOf_relations = res.metadata.relations.filter(type=RelationTypes.isPartOf).all()
+            if not isPartOf_relations:
+                continue
+            res_citation = res.get_citation()
+            for rel in isPartOf_relations:
+                # get the collection object from the relation...
+                try:
+                    citation = rel.value
+                    if "doi.org" in citation:
+                        id = rel.value.split("/hs.")[1][:32]
+                    else:
+                        id = rel.value.split("/resource/")[1][:32]
+                    col = BaseResource.objects.get(short_id=id)
+                except Exception as e:
+                    message = f"Unable to parse id from citation: {rel.value}, {e}"
+                    print(message)
+                    errors.append(message)
+                    continue
+                haspart_relations = col.metadata.relations.filter(type=RelationTypes.hasPart).all()
+                if res_citation not in [rel.value for rel in haspart_relations]:
+                    print(f"{i}/{res_count}:{res}, {current_site}/resource/{res.short_id} has isPart meta.")
+                    print(f"Collection {col}, {current_site}/resource/{col.short_id} is missing hasPart.")
+                    if dry_run:
+                        print("SKIPPING delete of isPartOf relation because dry_run")
+                    else:
+                        print("Removing dangling isPartOf relation...")
+                        res.metadata.delete_element('relation', rel.id)
+                    fixed = fixed + 1
+        print(f"{fixed} isPart relations found dangling")
+        if errors:
+            print("Errors:")
+            for error in errors:
+                print(error)
         print("DONE")
diff --git a/hs_collection_resource/views.py b/hs_collection_resource/views.py
--- a/hs_collection_resource/views.py
+++ b/hs_collection_resource/views.py
@@ -5,6 +5,7 @@
 from django.http import JsonResponse
 from django.template.loader import render_to_string
 from rest_framework import status as http_status
+from django.core.exceptions import ValidationError
 
 from hs_core.enums import RelationTypes
 from hs_core.hydroshare.utils import get_resource_by_shortkey, resource_modified, set_dirty_bag_flag
@@ -193,8 +194,12 @@ def update_collection(request, shortkey, *args, **kwargs):
                                                 set_res_modified=False)
 
                 # add relation meta element of type 'isPartOf' to the resource added to the collection
-                res_to_add.metadata.create_element('relation', type='isPartOf',
-                                                   value=collection_res_obj.get_citation())
+                try:
+                    res_to_add.metadata.create_element('relation', type='isPartOf',
+                                                       value=collection_res_obj.get_citation())
+                except ValidationError:
+                    # The isPartOf metadata already exists
+                    pass
                 set_dirty_bag_flag(res_to_add)
 
             if collection_res_obj.can_be_public_or_discoverable:


Test patch: