Repository: hydroshare/hydroshare

Issue:
versioning a resource fails when author is no longer hs user
**Description of the bug**
Try to version this resource: https://beta.hydroshare.org/resource/2836494ee75e43a9bfb647b37260e461/
```
Traceback (most recent call last):
  File "/hydroshare/hs_core/tasks.py", line 803, in create_new_version_resource_task
    utils.copy_and_create_metadata(ori_res, new_res)
  File "/hydroshare/hs_core/hydroshare/utils.py", line 553, in copy_and_create_metadata
    dest_res.metadata.copy_all_elements_from(src_res.metadata, exclude_elements)
  File "/hydroshare/hs_core/models.py", line 4637, in copy_all_elements_from
    self.create_element(element_name, **element_args)
  File "/hydroshare/hs_core/models.py", line 4788, in create_element
    element = model_type.model_class().create(**kwargs)
  File "/hydroshare/hs_core/models.py", line 551, in create
    validate_hydroshare_user_id(hs_user_id)
  File "/hydroshare/hs_core/models.py", line 137, in validate_hydroshare_user_id
    raise ValidationError(err_message)
django.core.exceptions.ValidationError: ['1829 is not a valid id for hydroshare user']

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.9/site-packages/celery/app/trace.py", line 451, in trace_task
    R = retval = fun(*args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/celery/app/trace.py", line 734, in __protected_call__
    return self.run(*args, **kwargs)
  File "/hydroshare/hs_core/tasks.py", line 836, in create_new_version_resource_task
    raise utils.ResourceVersioningException(str(ex))
hs_core.hydroshare.utils.ResourceVersioningException: ['1829 is not a valid id for hydroshare user']
```

The resourcemetadata.xml contains:
```
    <dc:creator>
      <rdf:Description>
        <hsterms:organization>UT Austin Center for Water and the Environment</hsterms:organization>
        <hsterms:phone></hsterms:phone>
        <hsterms:hydroshare_user_id rdf:datatype="http://www.w3.org/2001/XMLSchema#integer">1829</hsterms:hydroshare_user_id>
        <hsterms:description rdf:datatype="http://www.w3.org/2001/XMLSchema#integer">1829</hsterms:description>
        <hsterms:creatorOrder rdf:datatype="http://www.w3.org/2001/XMLSchema#integer">2</hsterms:creatorOrder>
        <hsterms:homepage></hsterms:homepage>
        <hsterms:address></hsterms:address>
        <hsterms:name>Erika Boghici</hsterms:name>
        <hsterms:email>e.boghici@austin.utexas.edu</hsterms:email>
      </rdf:Description>
    </dc:creator>
```
The `hydroshare_user_id` is no longer valid. That causes the versioning to fail

**Expected behavior**
Versioning should succeed

**Additional information**
HS 2.11.1



Discussion:
@martinseul suggested: "we'll need to prevent that a user who is an author on a published resource can be deleted"

Resolving patch:
diff --git a/hs_core/admin.py b/hs_core/admin.py
--- a/hs_core/admin.py
+++ b/hs_core/admin.py
@@ -1,11 +1,52 @@
 from django import forms
-from django.contrib.auth.admin import UserAdmin
+from django.contrib import messages
+from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
+from django.contrib.admin.actions import delete_selected as django_delete_selected
 from django.contrib.auth.forms import UserCreationForm
 from django.contrib.gis import admin
 from django.contrib.contenttypes.admin import GenericTabularInline
 from django.utils.translation import gettext_lazy as _
 
-from .models import ResourceFile, User
+from .models import ResourceFile, User, BaseResource, Creator
+
+
+class UserAdmin(DjangoUserAdmin):
+    actions = ['delete_selected']
+
+    def delete_model(self, request, obj):
+        # prevent user delete if user is an owner/author on a published resource
+        published_resources = None
+        creators = Creator.objects.filter(hydroshare_user_id=obj.id)
+        for c in creators:
+            published_resources = BaseResource.objects.filter(
+                object_id=c.content_object.id, raccess__published=True)
+        if published_resources:
+            res_ids = ", ".join(str(res.short_id) for res in published_resources)
+            message = f"Can't delete user. They are a creator of published resource(s): {res_ids}"
+            self.message_user(request, message, messages.ERROR)
+            # https://github.com/django/django/blob/3.2/django/contrib/admin/actions.py#L46-L48C27
+            return False
+        else:
+            return super(UserAdmin, self).delete_model(request, obj)
+
+    def delete_selected(self, request, queryset):
+        # prevent user delete if user is an owner/author on a published resource
+        user_no_del = []
+        for user in queryset:
+            creators = Creator.objects.filter(hydroshare_user_id=user.id)
+            for c in creators:
+                published_resources = BaseResource.objects.filter(
+                    object_id=c.content_object.id, raccess__published=True)
+                if published_resources:
+                    user_no_del.append(user)
+                    queryset = queryset.exclude(id=user.id)
+        if user_no_del:
+            usernames = ", ".join(str(u.username) for u in user_no_del)
+            message = f"Can't delete user(s): {usernames}. They are creator(s) of published resources."
+            self.message_user(request, message, messages.ERROR)
+        if queryset.count():
+            return django_delete_selected(self, request, queryset)
+    delete_selected.short_description = django_delete_selected.short_description
 
 
 class UserCreationFormExtended(UserCreationForm):
diff --git a/hs_core/models.py b/hs_core/models.py
--- a/hs_core/models.py
+++ b/hs_core/models.py
@@ -123,6 +123,10 @@ def get_user(request):
         return request.user
 
 
+class UserValidationError(ValidationError):
+    pass
+
+
 def validate_hydroshare_user_id(value):
     """Validate that a hydroshare_user_id is valid for a hydroshare user."""
     err_message = '%s is not a valid id for hydroshare user' % value
@@ -134,7 +138,7 @@ def validate_hydroshare_user_id(value):
 
         # check the user exists for the provided user id
         if not User.objects.filter(pk=value).exists():
-            raise ValidationError(err_message)
+            raise UserValidationError(err_message)
 
 
 def validate_user_url(value):
@@ -4621,6 +4625,7 @@ def delete_all_elements(self):
 
     def copy_all_elements_from(self, src_md, exclude_elements=None):
         """Copy all metadata elements from another resource."""
+        logger = logging.getLogger(__name__)
         md_type = ContentType.objects.get_for_model(src_md)
         supported_element_names = src_md.get_supported_element_names()
         for element_name in supported_element_names:
@@ -4632,10 +4637,16 @@ def copy_all_elements_from(self, src_md, exclude_elements=None):
                 element_args.pop('content_type')
                 element_args.pop('id')
                 element_args.pop('object_id')
-                if exclude_elements:
-                    if not element_name.lower() in exclude_elements:
+                try:
+                    if exclude_elements:
+                        if not element_name.lower() in exclude_elements:
+                            self.create_element(element_name, **element_args)
+                    else:
                         self.create_element(element_name, **element_args)
-                else:
+                except UserValidationError as uve:
+                    logger.error(f"Error copying {element}: {str(uve)}")
+                    element_args["hydroshare_user_id"] = None
+                    del element_args["is_active_user"]
                     self.create_element(element_name, **element_args)
 
     # this method needs to be overriden by any subclass of this class


Test patch:
diff --git a/hs_composite_resource/tests/test_composite_resource.py b/hs_composite_resource/tests/test_composite_resource.py
--- a/hs_composite_resource/tests/test_composite_resource.py
+++ b/hs_composite_resource/tests/test_composite_resource.py
@@ -3444,6 +3444,99 @@ def test_version_resource_immunity_unpublished(self):
         )
         self.assertFalse(ori_res.raccess.immutable)
 
+    def test_version_resource_published_deleted_user(self):
+        """Here we are testing versioning a published resource
+        when a creator's user account has been deleted.
+        """
+        self.create_composite_resource()
+        # add a file to the resource
+        self.add_file_to_resource(file_to_add=self.generic_file)
+
+        # add another creator
+        metadata = self.composite_resource.metadata
+        author_account = hydroshare.create_account(
+            "author@nowhere.com",
+            username="author",
+            password='mypassword1',
+            first_name="Creator_FirstName",
+            last_name="Creator_LastName",
+            superuser=False,
+            groups=[self.group],
+        )
+        creator = metadata.create_element("creator",
+                                          name=f"{author_account.last_name}, {author_account.first_name}",
+                                          email=author_account.email,
+                                          hydroshare_user_id=author_account.id)
+
+        # make the original resource published before versioning
+        self.composite_resource.raccess.immutable = True
+        self.composite_resource.raccess.published = True
+        self.composite_resource.raccess.save()
+
+        # Delete the creator's user account
+        author_account.delete()
+
+        # Version the resource
+        new_composite_resource = hydroshare.create_empty_resource(
+            self.composite_resource.short_id, self.user
+        )
+        new_composite_resource = hydroshare.create_new_version_resource(
+            self.composite_resource, new_composite_resource, self.user
+        )
+        # there should be 2 creators now
+        self.assertEqual(new_composite_resource.metadata.creators.count(), 2)
+
+        creator_to_check = new_composite_resource.metadata.creators.all().filter(email=author_account.email).first()
+        self.assertNotEqual(creator_to_check, None)
+        self.assertEqual(creator_to_check.name, creator.name)
+        self.assertEqual(creator_to_check.email, creator.email)
+        self.assertIsNone(creator_to_check.hydroshare_user_id)
+        self.assertFalse(creator_to_check.is_active_user)
+
+    def test_version_resource_unpublished_deleted_user(self):
+        """Here we are testing versioning an unpublished resource
+        when a creator's user account has been deleted.
+        """
+        self.create_composite_resource()
+        # add a file to the resource
+        self.add_file_to_resource(file_to_add=self.generic_file)
+
+        # add another creator
+        metadata = self.composite_resource.metadata
+        author_account = hydroshare.create_account(
+            "author@nowhere.com",
+            username="author",
+            password='mypassword1',
+            first_name="Creator_FirstName",
+            last_name="Creator_LastName",
+            superuser=False,
+            groups=[self.group],
+        )
+        creator = metadata.create_element("creator",
+                                          name=f"{author_account.last_name}, {author_account.first_name}",
+                                          email=author_account.email,
+                                          hydroshare_user_id=author_account.id)
+
+        # Delete the creator's user account
+        author_account.delete()
+
+        # Version the resource
+        new_composite_resource = hydroshare.create_empty_resource(
+            self.composite_resource.short_id, self.user
+        )
+        new_composite_resource = hydroshare.create_new_version_resource(
+            self.composite_resource, new_composite_resource, self.user
+        )
+        # there should be 2 creators now
+        self.assertEqual(new_composite_resource.metadata.creators.count(), 2)
+
+        creator_to_check = new_composite_resource.metadata.creators.all().filter(email=author_account.email).first()
+        self.assertNotEqual(creator_to_check, None)
+        self.assertEqual(creator_to_check.name, creator.name)
+        self.assertEqual(creator_to_check.email, creator.email)
+        self.assertIsNone(creator_to_check.hydroshare_user_id)
+        self.assertFalse(creator_to_check.is_active_user)
+
     def test_version_resource_immunity_published(self):
         self.create_composite_resource()
         # add a file to the resource