We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Cannot delete users who have uploaded shared files
## Description
Attempting to delete a user who has uploaded files will give an error like so

```
Cannot delete user
Deleting the selected user would require deleting the following protected related objects:

Data entry: acBie9x4 WieezMsPbKL4P2
Data entry: KoBo question set
Data entry: myfile
Data entry: plus+sign+cool
Data entry: acBie9x4WieezMsPbKL4P2

```


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `terraso_backend/apps/core/models/users.py`
Content:
```
1 import uuid
2 
3 from django.contrib.auth.models import AbstractUser, BaseUserManager
4 from django.db import models
5 from safedelete.models import SOFT_DELETE_CASCADE, SafeDeleteManager, SafeDeleteModel
6 
7 
8 class UserManager(SafeDeleteManager, BaseUserManager):
9     use_in_migrations = True
10 
11     def _create_user(self, email, password, **extra_fields):
12         """Create and save a User with the given email and password."""
13         if not email:
14             raise ValueError("The given email must be set")
15 
16         email = self.normalize_email(email)
17         user = self.model(email=email, **extra_fields)
18         user.set_password(password)
19         user.save(using=self._db)
20 
21         return user
22 
23     def create_user(self, email, password=None, **extra_fields):
24         """Create and save a regular User with the given email and password."""
25         extra_fields.setdefault("is_staff", False)
26         extra_fields.setdefault("is_superuser", False)
27         return self._create_user(email, password, **extra_fields)
28 
29     def create_superuser(self, email, password, **extra_fields):
30         """Create and save a SuperUser with the given email and password."""
31         extra_fields.setdefault("is_staff", True)
32         extra_fields.setdefault("is_superuser", True)
33 
34         if extra_fields.get("is_staff") is not True:
35             raise ValueError("Superuser must have is_staff=True.")
36         if extra_fields.get("is_superuser") is not True:
37             raise ValueError("Superuser must have is_superuser=True.")
38 
39         return self._create_user(email, password, **extra_fields)
40 
41 
42 class User(SafeDeleteModel, AbstractUser):
43     """This model represents a User on Terraso platform."""
44 
45     fields_to_trim = ["first_name", "last_name"]
46 
47     _safedelete_policy = SOFT_DELETE_CASCADE
48 
49     id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
50     created_at = models.DateTimeField(auto_now_add=True)
51     updated_at = models.DateTimeField(auto_now=True)
52 
53     username = None
54     email = models.EmailField()
55     profile_image = models.URLField(blank=True, default="")
56 
57     USERNAME_FIELD = "email"
58     REQUIRED_FIELDS = []
59 
60     objects = UserManager()
61 
62     class Meta:
63         get_latest_by = "created_at"
64         ordering = ["-created_at"]
65         constraints = (
66             models.UniqueConstraint(
67                 fields=("email",),
68                 condition=models.Q(deleted_at__isnull=True),
69                 name="unique_active_email",
70             ),
71         )
72 
73     def save(self, *args, **kwargs):
74         for field in self.fields_to_trim:
75             setattr(self, field, getattr(self, field).strip())
76         return super().save(*args, **kwargs)
77 
78     def is_landscape_manager(self, landscape_id):
79         return (
80             self.memberships.managers_only()
81             .filter(
82                 group__associated_landscapes__is_default_landscape_group=True,
83                 group__associated_landscapes__landscape__pk=landscape_id,
84             )
85             .exists()
86         )
87 
88     def is_group_manager(self, group_id):
89         return self.memberships.managers_only().filter(group__pk=group_id).exists()
90 
91     def __str__(self):
92         return self.email
93 
94 
95 class UserPreference(models.Model):
96     id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
97     created_at = models.DateTimeField(auto_now_add=True)
98     updated_at = models.DateTimeField(auto_now=True)
99     key = models.CharField(max_length=128)
100     value = models.CharField(max_length=512, blank=True, default="")
101 
102     user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="preferences")
103 
104     class Meta:
105         constraints = (
106             models.UniqueConstraint(
107                 fields=("key", "user"),
108                 name="unique_user_preference",
109             ),
110         )
111
```

Path: `terraso_backend/apps/shared_data/models/data_entries.py`
Content:
```
1 from django.db import models
2 from django.utils import timezone
3 from django.utils.translation import gettext_lazy as _
4 
5 from apps.core.models import BaseModel, Group, User
6 from apps.shared_data import permission_rules as perm_rules
7 from apps.shared_data.services import DataEntryFileStorage
8 
9 
10 class DataEntry(BaseModel):
11     """
12     Data Entry stores information about resources (usually files) that contain
13     different kind of data used by Landscape managers. Common resource types are
14     csv, xls and JSON files.
15 
16     A Data Entry can point to internal or external resources. An internal
17     resource is stored on Terraso's infrastructure and an external resource is
18     stored out of the Terraso's infrastructure. In both cases, the Data Entry
19     only has the URL for that resource as a link to it.
20 
21     Attributes
22     ----------
23     name: str
24         any user given name for that resource
25     description: str
26         a longer description explaining the resource
27     resource_type: str
28         the 'technical' type of the resource, usually the mime type
29     url: str
30         the URL where the resource can be accessed
31 
32     groups: ManyToManyField(Group)
33         Groups where the resource is linked to (shared)
34     created_by: User
35         User who created the resource
36     """
37 
38     name = models.CharField(max_length=128)
39     description = models.TextField(blank=True, default="")
40 
41     ENTRY_TYPE_FILE = "file"
42     ENTRY_TYPE_LINK = "link"
43     MEMBERSHIP_TYPES = (
44         (ENTRY_TYPE_FILE, _("File")),
45         (ENTRY_TYPE_LINK, _("Link")),
46     )
47     entry_type = models.CharField(
48         max_length=32,
49         choices=MEMBERSHIP_TYPES,
50     )
51 
52     resource_type = models.CharField(max_length=255, blank=True, default="")
53     url = models.URLField()
54     size = models.PositiveBigIntegerField(null=True, blank=True)
55 
56     groups = models.ManyToManyField(Group, related_name="data_entries")
57     created_by = models.ForeignKey(User, on_delete=models.PROTECT)
58     file_removed_at = models.DateTimeField(blank=True, null=True)
59 
60     class Meta(BaseModel.Meta):
61         verbose_name_plural = "Data Entries"
62         rules_permissions = {
63             "change": perm_rules.allowed_to_change_data_entry,
64             "delete": perm_rules.allowed_to_delete_data_entry,
65             "view": perm_rules.allowed_to_view_data_entry,
66         }
67 
68     @property
69     def s3_object_name(self):
70         object_name = "/".join(self.url.split("/")[-2:]) if self.url else ""
71 
72         # We want to put back the space character so the sign url works properly
73         object_name = object_name.replace("%20", " ")
74         return object_name
75 
76     @property
77     def signed_url(self):
78         storage = DataEntryFileStorage(custom_domain=None)
79         return storage.url(self.s3_object_name)
80 
81     def delete_file_on_storage(self):
82         if not self.deleted_at:
83             raise RuntimeError(
84                 f"Storage object cannot be deleted if its DataEntry ({self.id}) is not deleted."
85             )
86 
87         if self.file_removed_at:
88             return
89 
90         storage = DataEntryFileStorage(custom_domain=None)
91         storage.delete(self.s3_object_name)
92         self.file_removed_at = timezone.now()
93         self.save(keep_deleted=True)
94 
95     def to_dict(self):
96         return dict(
97             id=str(self.id),
98             name=self.name,
99             entry_type=self.entry_type,
100             description=self.description,
101             url=self.signed_url,
102             resource_type=self.resource_type,
103             size=self.size,
104             created_by=str(self.created_by.id),
105             groups=[str(group.id) for group in self.groups.all()],
106         )
107 
108     def __str__(self):
109         return self.name
110 
111     @classmethod
112     def get_entry_type_from_text(cls, entry_type):
113         if entry_type and entry_type.lower() == cls.ENTRY_TYPE_FILE:
114             return cls.ENTRY_TYPE_FILE
115         return cls.ENTRY_TYPE_LINK
116
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/terraso_backend/apps/core/models/users.py b/terraso_backend/apps/core/models/users.py
--- a/terraso_backend/apps/core/models/users.py
+++ b/terraso_backend/apps/core/models/users.py
@@ -85,6 +85,17 @@
             .exists()
         )
 
+    def soft_delete_policy_action(self, **kwargs):
+        """Relink files to deleted user. The default policy is to set the `created_by` field to
+        null if the user is deleted. However, for a soft deletion we want to keep this link. That
+        way if the user is restored, the created_by is still pointing to the same place."""
+        linked_dataentries = self.dataentry_set.all()
+        delete_response = super().soft_delete_policy_action()
+        for entry in linked_dataentries:
+            entry.created_by = self
+            entry.save()
+        return delete_response
+
     def is_group_manager(self, group_id):
         return self.memberships.managers_only().filter(group__pk=group_id).exists()
 
diff --git a/terraso_backend/apps/shared_data/models/data_entries.py b/terraso_backend/apps/shared_data/models/data_entries.py
--- a/terraso_backend/apps/shared_data/models/data_entries.py
+++ b/terraso_backend/apps/shared_data/models/data_entries.py
@@ -1,6 +1,7 @@
 from django.db import models
 from django.utils import timezone
 from django.utils.translation import gettext_lazy as _
+from safedelete.models import SOFT_DELETE
 
 from apps.core.models import BaseModel, Group, User
 from apps.shared_data import permission_rules as perm_rules
@@ -35,6 +36,9 @@
         User who created the resource
     """
 
+    # file will not be deleted in cascade
+    _safedelete_policy = SOFT_DELETE
+
     name = models.CharField(max_length=128)
     description = models.TextField(blank=True, default="")
 
@@ -54,7 +58,7 @@
     size = models.PositiveBigIntegerField(null=True, blank=True)
 
     groups = models.ManyToManyField(Group, related_name="data_entries")
-    created_by = models.ForeignKey(User, on_delete=models.PROTECT)
+    created_by = models.ForeignKey(User, null=True, on_delete=models.DO_NOTHING)
     file_removed_at = models.DateTimeField(blank=True, null=True)
 
     class Meta(BaseModel.Meta):

{"golden_diff": "diff --git a/terraso_backend/apps/core/models/users.py b/terraso_backend/apps/core/models/users.py\n--- a/terraso_backend/apps/core/models/users.py\n+++ b/terraso_backend/apps/core/models/users.py\n@@ -85,6 +85,17 @@\n             .exists()\n         )\n \n+    def soft_delete_policy_action(self, **kwargs):\n+        \"\"\"Relink files to deleted user. The default policy is to set the `created_by` field to\n+        null if the user is deleted. However, for a soft deletion we want to keep this link. That\n+        way if the user is restored, the created_by is still pointing to the same place.\"\"\"\n+        linked_dataentries = self.dataentry_set.all()\n+        delete_response = super().soft_delete_policy_action()\n+        for entry in linked_dataentries:\n+            entry.created_by = self\n+            entry.save()\n+        return delete_response\n+\n     def is_group_manager(self, group_id):\n         return self.memberships.managers_only().filter(group__pk=group_id).exists()\n \ndiff --git a/terraso_backend/apps/shared_data/models/data_entries.py b/terraso_backend/apps/shared_data/models/data_entries.py\n--- a/terraso_backend/apps/shared_data/models/data_entries.py\n+++ b/terraso_backend/apps/shared_data/models/data_entries.py\n@@ -1,6 +1,7 @@\n from django.db import models\n from django.utils import timezone\n from django.utils.translation import gettext_lazy as _\n+from safedelete.models import SOFT_DELETE\n \n from apps.core.models import BaseModel, Group, User\n from apps.shared_data import permission_rules as perm_rules\n@@ -35,6 +36,9 @@\n         User who created the resource\n     \"\"\"\n \n+    # file will not be deleted in cascade\n+    _safedelete_policy = SOFT_DELETE\n+\n     name = models.CharField(max_length=128)\n     description = models.TextField(blank=True, default=\"\")\n \n@@ -54,7 +58,7 @@\n     size = models.PositiveBigIntegerField(null=True, blank=True)\n \n     groups = models.ManyToManyField(Group, related_name=\"data_entries\")\n-    created_by = models.ForeignKey(User, on_delete=models.PROTECT)\n+    created_by = models.ForeignKey(User, null=True, on_delete=models.DO_NOTHING)\n     file_removed_at = models.DateTimeField(blank=True, null=True)\n \n     class Meta(BaseModel.Meta):\n", "issue": "Cannot delete users who have uploaded shared files\n## Description\r\nAttempting to delete a user who has uploaded files will give an error like so\r\n\r\n```\r\nCannot delete user\r\nDeleting the selected user would require deleting the following protected related objects:\r\n\r\nData entry: acBie9x4 WieezMsPbKL4P2\r\nData entry: KoBo question set\r\nData entry: myfile\r\nData entry: plus+sign+cool\r\nData entry: acBie9x4WieezMsPbKL4P2\r\n\r\n```\r\n\n", "before_files": [{"content": "import uuid\n\nfrom django.contrib.auth.models import AbstractUser, BaseUserManager\nfrom django.db import models\nfrom safedelete.models import SOFT_DELETE_CASCADE, SafeDeleteManager, SafeDeleteModel\n\n\nclass UserManager(SafeDeleteManager, BaseUserManager):\n    use_in_migrations = True\n\n    def _create_user(self, email, password, **extra_fields):\n        \"\"\"Create and save a User with the given email and password.\"\"\"\n        if not email:\n            raise ValueError(\"The given email must be set\")\n\n        email = self.normalize_email(email)\n        user = self.model(email=email, **extra_fields)\n        user.set_password(password)\n        user.save(using=self._db)\n\n        return user\n\n    def create_user(self, email, password=None, **extra_fields):\n        \"\"\"Create and save a regular User with the given email and password.\"\"\"\n        extra_fields.setdefault(\"is_staff\", False)\n        extra_fields.setdefault(\"is_superuser\", False)\n        return self._create_user(email, password, **extra_fields)\n\n    def create_superuser(self, email, password, **extra_fields):\n        \"\"\"Create and save a SuperUser with the given email and password.\"\"\"\n        extra_fields.setdefault(\"is_staff\", True)\n        extra_fields.setdefault(\"is_superuser\", True)\n\n        if extra_fields.get(\"is_staff\") is not True:\n            raise ValueError(\"Superuser must have is_staff=True.\")\n        if extra_fields.get(\"is_superuser\") is not True:\n            raise ValueError(\"Superuser must have is_superuser=True.\")\n\n        return self._create_user(email, password, **extra_fields)\n\n\nclass User(SafeDeleteModel, AbstractUser):\n    \"\"\"This model represents a User on Terraso platform.\"\"\"\n\n    fields_to_trim = [\"first_name\", \"last_name\"]\n\n    _safedelete_policy = SOFT_DELETE_CASCADE\n\n    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)\n    created_at = models.DateTimeField(auto_now_add=True)\n    updated_at = models.DateTimeField(auto_now=True)\n\n    username = None\n    email = models.EmailField()\n    profile_image = models.URLField(blank=True, default=\"\")\n\n    USERNAME_FIELD = \"email\"\n    REQUIRED_FIELDS = []\n\n    objects = UserManager()\n\n    class Meta:\n        get_latest_by = \"created_at\"\n        ordering = [\"-created_at\"]\n        constraints = (\n            models.UniqueConstraint(\n                fields=(\"email\",),\n                condition=models.Q(deleted_at__isnull=True),\n                name=\"unique_active_email\",\n            ),\n        )\n\n    def save(self, *args, **kwargs):\n        for field in self.fields_to_trim:\n            setattr(self, field, getattr(self, field).strip())\n        return super().save(*args, **kwargs)\n\n    def is_landscape_manager(self, landscape_id):\n        return (\n            self.memberships.managers_only()\n            .filter(\n                group__associated_landscapes__is_default_landscape_group=True,\n                group__associated_landscapes__landscape__pk=landscape_id,\n            )\n            .exists()\n        )\n\n    def is_group_manager(self, group_id):\n        return self.memberships.managers_only().filter(group__pk=group_id).exists()\n\n    def __str__(self):\n        return self.email\n\n\nclass UserPreference(models.Model):\n    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)\n    created_at = models.DateTimeField(auto_now_add=True)\n    updated_at = models.DateTimeField(auto_now=True)\n    key = models.CharField(max_length=128)\n    value = models.CharField(max_length=512, blank=True, default=\"\")\n\n    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name=\"preferences\")\n\n    class Meta:\n        constraints = (\n            models.UniqueConstraint(\n                fields=(\"key\", \"user\"),\n                name=\"unique_user_preference\",\n            ),\n        )\n", "path": "terraso_backend/apps/core/models/users.py"}, {"content": "from django.db import models\nfrom django.utils import timezone\nfrom django.utils.translation import gettext_lazy as _\n\nfrom apps.core.models import BaseModel, Group, User\nfrom apps.shared_data import permission_rules as perm_rules\nfrom apps.shared_data.services import DataEntryFileStorage\n\n\nclass DataEntry(BaseModel):\n    \"\"\"\n    Data Entry stores information about resources (usually files) that contain\n    different kind of data used by Landscape managers. Common resource types are\n    csv, xls and JSON files.\n\n    A Data Entry can point to internal or external resources. An internal\n    resource is stored on Terraso's infrastructure and an external resource is\n    stored out of the Terraso's infrastructure. In both cases, the Data Entry\n    only has the URL for that resource as a link to it.\n\n    Attributes\n    ----------\n    name: str\n        any user given name for that resource\n    description: str\n        a longer description explaining the resource\n    resource_type: str\n        the 'technical' type of the resource, usually the mime type\n    url: str\n        the URL where the resource can be accessed\n\n    groups: ManyToManyField(Group)\n        Groups where the resource is linked to (shared)\n    created_by: User\n        User who created the resource\n    \"\"\"\n\n    name = models.CharField(max_length=128)\n    description = models.TextField(blank=True, default=\"\")\n\n    ENTRY_TYPE_FILE = \"file\"\n    ENTRY_TYPE_LINK = \"link\"\n    MEMBERSHIP_TYPES = (\n        (ENTRY_TYPE_FILE, _(\"File\")),\n        (ENTRY_TYPE_LINK, _(\"Link\")),\n    )\n    entry_type = models.CharField(\n        max_length=32,\n        choices=MEMBERSHIP_TYPES,\n    )\n\n    resource_type = models.CharField(max_length=255, blank=True, default=\"\")\n    url = models.URLField()\n    size = models.PositiveBigIntegerField(null=True, blank=True)\n\n    groups = models.ManyToManyField(Group, related_name=\"data_entries\")\n    created_by = models.ForeignKey(User, on_delete=models.PROTECT)\n    file_removed_at = models.DateTimeField(blank=True, null=True)\n\n    class Meta(BaseModel.Meta):\n        verbose_name_plural = \"Data Entries\"\n        rules_permissions = {\n            \"change\": perm_rules.allowed_to_change_data_entry,\n            \"delete\": perm_rules.allowed_to_delete_data_entry,\n            \"view\": perm_rules.allowed_to_view_data_entry,\n        }\n\n    @property\n    def s3_object_name(self):\n        object_name = \"/\".join(self.url.split(\"/\")[-2:]) if self.url else \"\"\n\n        # We want to put back the space character so the sign url works properly\n        object_name = object_name.replace(\"%20\", \" \")\n        return object_name\n\n    @property\n    def signed_url(self):\n        storage = DataEntryFileStorage(custom_domain=None)\n        return storage.url(self.s3_object_name)\n\n    def delete_file_on_storage(self):\n        if not self.deleted_at:\n            raise RuntimeError(\n                f\"Storage object cannot be deleted if its DataEntry ({self.id}) is not deleted.\"\n            )\n\n        if self.file_removed_at:\n            return\n\n        storage = DataEntryFileStorage(custom_domain=None)\n        storage.delete(self.s3_object_name)\n        self.file_removed_at = timezone.now()\n        self.save(keep_deleted=True)\n\n    def to_dict(self):\n        return dict(\n            id=str(self.id),\n            name=self.name,\n            entry_type=self.entry_type,\n            description=self.description,\n            url=self.signed_url,\n            resource_type=self.resource_type,\n            size=self.size,\n            created_by=str(self.created_by.id),\n            groups=[str(group.id) for group in self.groups.all()],\n        )\n\n    def __str__(self):\n        return self.name\n\n    @classmethod\n    def get_entry_type_from_text(cls, entry_type):\n        if entry_type and entry_type.lower() == cls.ENTRY_TYPE_FILE:\n            return cls.ENTRY_TYPE_FILE\n        return cls.ENTRY_TYPE_LINK\n", "path": "terraso_backend/apps/shared_data/models/data_entries.py"}], "after_files": [{"content": "import uuid\n\nfrom django.contrib.auth.models import AbstractUser, BaseUserManager\nfrom django.db import models\nfrom safedelete.models import SOFT_DELETE_CASCADE, SafeDeleteManager, SafeDeleteModel\n\n\nclass UserManager(SafeDeleteManager, BaseUserManager):\n    use_in_migrations = True\n\n    def _create_user(self, email, password, **extra_fields):\n        \"\"\"Create and save a User with the given email and password.\"\"\"\n        if not email:\n            raise ValueError(\"The given email must be set\")\n\n        email = self.normalize_email(email)\n        user = self.model(email=email, **extra_fields)\n        user.set_password(password)\n        user.save(using=self._db)\n\n        return user\n\n    def create_user(self, email, password=None, **extra_fields):\n        \"\"\"Create and save a regular User with the given email and password.\"\"\"\n        extra_fields.setdefault(\"is_staff\", False)\n        extra_fields.setdefault(\"is_superuser\", False)\n        return self._create_user(email, password, **extra_fields)\n\n    def create_superuser(self, email, password, **extra_fields):\n        \"\"\"Create and save a SuperUser with the given email and password.\"\"\"\n        extra_fields.setdefault(\"is_staff\", True)\n        extra_fields.setdefault(\"is_superuser\", True)\n\n        if extra_fields.get(\"is_staff\") is not True:\n            raise ValueError(\"Superuser must have is_staff=True.\")\n        if extra_fields.get(\"is_superuser\") is not True:\n            raise ValueError(\"Superuser must have is_superuser=True.\")\n\n        return self._create_user(email, password, **extra_fields)\n\n\nclass User(SafeDeleteModel, AbstractUser):\n    \"\"\"This model represents a User on Terraso platform.\"\"\"\n\n    fields_to_trim = [\"first_name\", \"last_name\"]\n\n    _safedelete_policy = SOFT_DELETE_CASCADE\n\n    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)\n    created_at = models.DateTimeField(auto_now_add=True)\n    updated_at = models.DateTimeField(auto_now=True)\n\n    username = None\n    email = models.EmailField()\n    profile_image = models.URLField(blank=True, default=\"\")\n\n    USERNAME_FIELD = \"email\"\n    REQUIRED_FIELDS = []\n\n    objects = UserManager()\n\n    class Meta:\n        get_latest_by = \"created_at\"\n        ordering = [\"-created_at\"]\n        constraints = (\n            models.UniqueConstraint(\n                fields=(\"email\",),\n                condition=models.Q(deleted_at__isnull=True),\n                name=\"unique_active_email\",\n            ),\n        )\n\n    def save(self, *args, **kwargs):\n        for field in self.fields_to_trim:\n            setattr(self, field, getattr(self, field).strip())\n        return super().save(*args, **kwargs)\n\n    def is_landscape_manager(self, landscape_id):\n        return (\n            self.memberships.managers_only()\n            .filter(\n                group__associated_landscapes__is_default_landscape_group=True,\n                group__associated_landscapes__landscape__pk=landscape_id,\n            )\n            .exists()\n        )\n\n    def soft_delete_policy_action(self, **kwargs):\n        \"\"\"Relink files to deleted user. The default policy is to set the `created_by` field to\n        null if the user is deleted. However, for a soft deletion we want to keep this link. That\n        way if the user is restored, the created_by is still pointing to the same place.\"\"\"\n        linked_dataentries = self.dataentry_set.all()\n        delete_response = super().soft_delete_policy_action()\n        for entry in linked_dataentries:\n            entry.created_by = self\n            entry.save()\n        return delete_response\n\n    def is_group_manager(self, group_id):\n        return self.memberships.managers_only().filter(group__pk=group_id).exists()\n\n    def __str__(self):\n        return self.email\n\n\nclass UserPreference(models.Model):\n    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)\n    created_at = models.DateTimeField(auto_now_add=True)\n    updated_at = models.DateTimeField(auto_now=True)\n    key = models.CharField(max_length=128)\n    value = models.CharField(max_length=512, blank=True, default=\"\")\n\n    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name=\"preferences\")\n\n    class Meta:\n        constraints = (\n            models.UniqueConstraint(\n                fields=(\"key\", \"user\"),\n                name=\"unique_user_preference\",\n            ),\n        )\n", "path": "terraso_backend/apps/core/models/users.py"}, {"content": "from django.db import models\nfrom django.utils import timezone\nfrom django.utils.translation import gettext_lazy as _\nfrom safedelete.models import SOFT_DELETE\n\nfrom apps.core.models import BaseModel, Group, User\nfrom apps.shared_data import permission_rules as perm_rules\nfrom apps.shared_data.services import DataEntryFileStorage\n\n\nclass DataEntry(BaseModel):\n    \"\"\"\n    Data Entry stores information about resources (usually files) that contain\n    different kind of data used by Landscape managers. Common resource types are\n    csv, xls and JSON files.\n\n    A Data Entry can point to internal or external resources. An internal\n    resource is stored on Terraso's infrastructure and an external resource is\n    stored out of the Terraso's infrastructure. In both cases, the Data Entry\n    only has the URL for that resource as a link to it.\n\n    Attributes\n    ----------\n    name: str\n        any user given name for that resource\n    description: str\n        a longer description explaining the resource\n    resource_type: str\n        the 'technical' type of the resource, usually the mime type\n    url: str\n        the URL where the resource can be accessed\n\n    groups: ManyToManyField(Group)\n        Groups where the resource is linked to (shared)\n    created_by: User\n        User who created the resource\n    \"\"\"\n\n    # file will not be deleted in cascade\n    _safedelete_policy = SOFT_DELETE\n\n    name = models.CharField(max_length=128)\n    description = models.TextField(blank=True, default=\"\")\n\n    ENTRY_TYPE_FILE = \"file\"\n    ENTRY_TYPE_LINK = \"link\"\n    MEMBERSHIP_TYPES = (\n        (ENTRY_TYPE_FILE, _(\"File\")),\n        (ENTRY_TYPE_LINK, _(\"Link\")),\n    )\n    entry_type = models.CharField(\n        max_length=32,\n        choices=MEMBERSHIP_TYPES,\n    )\n\n    resource_type = models.CharField(max_length=255, blank=True, default=\"\")\n    url = models.URLField()\n    size = models.PositiveBigIntegerField(null=True, blank=True)\n\n    groups = models.ManyToManyField(Group, related_name=\"data_entries\")\n    created_by = models.ForeignKey(User, null=True, on_delete=models.DO_NOTHING)\n    file_removed_at = models.DateTimeField(blank=True, null=True)\n\n    class Meta(BaseModel.Meta):\n        verbose_name_plural = \"Data Entries\"\n        rules_permissions = {\n            \"change\": perm_rules.allowed_to_change_data_entry,\n            \"delete\": perm_rules.allowed_to_delete_data_entry,\n            \"view\": perm_rules.allowed_to_view_data_entry,\n        }\n\n    @property\n    def s3_object_name(self):\n        object_name = \"/\".join(self.url.split(\"/\")[-2:]) if self.url else \"\"\n\n        # We want to put back the space character so the sign url works properly\n        object_name = object_name.replace(\"%20\", \" \")\n        return object_name\n\n    @property\n    def signed_url(self):\n        storage = DataEntryFileStorage(custom_domain=None)\n        return storage.url(self.s3_object_name)\n\n    def delete_file_on_storage(self):\n        if not self.deleted_at:\n            raise RuntimeError(\n                f\"Storage object cannot be deleted if its DataEntry ({self.id}) is not deleted.\"\n            )\n\n        if self.file_removed_at:\n            return\n\n        storage = DataEntryFileStorage(custom_domain=None)\n        storage.delete(self.s3_object_name)\n        self.file_removed_at = timezone.now()\n        self.save(keep_deleted=True)\n\n    def to_dict(self):\n        return dict(\n            id=str(self.id),\n            name=self.name,\n            entry_type=self.entry_type,\n            description=self.description,\n            url=self.signed_url,\n            resource_type=self.resource_type,\n            size=self.size,\n            created_by=str(self.created_by.id),\n            groups=[str(group.id) for group in self.groups.all()],\n        )\n\n    def __str__(self):\n        return self.name\n\n    @classmethod\n    def get_entry_type_from_text(cls, entry_type):\n        if entry_type and entry_type.lower() == cls.ENTRY_TYPE_FILE:\n            return cls.ENTRY_TYPE_FILE\n        return cls.ENTRY_TYPE_LINK\n", "path": "terraso_backend/apps/shared_data/models/data_entries.py"}]}