Repository: techmatters/terraso-backend

Issue:
Landscape name duplication error 
<!--
Use a concise title that describes the problem. Include key words from error messages.
Bad: Problem with newsletter form
Good: Newsletter form missing email address field

Bad: Issue with website
Good: Footer missing from homepage
-->

## Description
Got an error message when trying to create a new landscape 'COBIOCOM' on staging env.
## Steps To Reproduce
<!--
Steps to reproduce the behavior. Describe what you typed, clicked, tapped, swiped, saw and heard.
-->
1. Go to URL: https://app.staging.terraso.org/landscapes?search=cobiocom
2. Notice no search result for COBIOCOM
3. Scroll down to create a landscape under COBIOCOM and click 'Create Landscape' button
4. Fill the profile form (Step1)
5. Choose to skip for now option (Step2)
6. Notice the error message

## Expected behavior
The new landscape page gets created with a generic map

## Actual behavior
<img width="471" alt="image" src="https://user-images.githubusercontent.com/92342777/177654152-f17ca3f4-c3e2-46a3-a8f0-3ac3350a1f56.png">
## Additional context
<!--
Add context about the problem.
Include other URLs related to the problem that should also work correctly.
If applicable, add screenshots.
-->



Discussion:
@josebui This looks like a soft delete-related error.
Another deletion bug: https://github.com/techmatters/terraso-backend/issues/190

Resolving patch:
diff --git a/terraso_backend/apps/core/migrations/0029_alter_group_deleted_at_and_more.py b/terraso_backend/apps/core/migrations/0029_alter_group_deleted_at_and_more.py
new file mode 100644
--- /dev/null
+++ b/terraso_backend/apps/core/migrations/0029_alter_group_deleted_at_and_more.py
@@ -0,0 +1,45 @@
+# Generated by Django 4.1.3 on 2022-12-13 20:26
+
+from django.db import migrations, models
+
+import apps.core.models.commons
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("core", "0028_spanish_taxonomy_terms"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="landscape",
+            name="name",
+            field=models.CharField(
+                max_length=128, validators=[apps.core.models.commons.validate_name]
+            ),
+        ),
+        migrations.AddConstraint(
+            model_name="landscape",
+            constraint=models.UniqueConstraint(
+                condition=models.Q(("deleted_at__isnull", True)),
+                fields=("name",),
+                name="core_landscape_name_key",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="group",
+            name="name",
+            field=models.CharField(
+                max_length=128, validators=[apps.core.models.commons.validate_name]
+            ),
+        ),
+        migrations.AddConstraint(
+            model_name="group",
+            constraint=models.UniqueConstraint(
+                condition=models.Q(("deleted_at__isnull", True)),
+                fields=("name",),
+                name="core_group_name_key",
+            ),
+        ),
+    ]
diff --git a/terraso_backend/apps/core/models/groups.py b/terraso_backend/apps/core/models/groups.py
--- a/terraso_backend/apps/core/models/groups.py
+++ b/terraso_backend/apps/core/models/groups.py
@@ -34,7 +34,7 @@ class Group(SlugModel):
 
     fields_to_trim = ["name", "description"]
 
-    name = models.CharField(max_length=128, unique=True, validators=[validate_name])
+    name = models.CharField(max_length=128, validators=[validate_name])
     description = models.TextField(max_length=512, blank=True, default="")
     website = models.URLField(blank=True, default="")
     email = models.EmailField(blank=True, default="")
@@ -70,6 +70,13 @@ class Meta(SlugModel.Meta):
             "change": perm_rules.allowed_to_change_group,
             "delete": perm_rules.allowed_to_delete_group,
         }
+        constraints = SlugModel.Meta.constraints + (
+            models.UniqueConstraint(
+                fields=("name",),
+                condition=models.Q(deleted_at__isnull=True),
+                name="core_group_name_key",
+            ),
+        )
 
     def save(self, *args, **kwargs):
         with transaction.atomic():
diff --git a/terraso_backend/apps/core/models/landscapes.py b/terraso_backend/apps/core/models/landscapes.py
--- a/terraso_backend/apps/core/models/landscapes.py
+++ b/terraso_backend/apps/core/models/landscapes.py
@@ -29,7 +29,7 @@ class Landscape(SlugModel, DirtyFieldsMixin):
 
     fields_to_trim = ["name", "description"]
 
-    name = models.CharField(max_length=128, unique=True, validators=[validate_name])
+    name = models.CharField(max_length=128, validators=[validate_name])
     description = models.TextField(blank=True, default="")
     website = models.URLField(blank=True, default="")
     location = models.CharField(max_length=128, blank=True, default="")
@@ -75,6 +75,13 @@ class Meta(SlugModel.Meta):
             "change": perm_rules.allowed_to_change_landscape,
             "delete": perm_rules.allowed_to_delete_landscape,
         }
+        constraints = SlugModel.Meta.constraints + (
+            models.UniqueConstraint(
+                fields=["name"],
+                condition=models.Q(deleted_at__isnull=True),
+                name="core_landscape_name_key",
+            ),
+        )
 
     def save(self, *args, **kwargs):
         dirty_fields = self.get_dirty_fields()
@@ -100,6 +107,17 @@ def save(self, *args, **kwargs):
                 )
                 landscape_group.save()
 
+    def delete(self, *args, **kwargs):
+
+        default_group = self.get_default_group()
+
+        with transaction.atomic():
+            ret = super().delete(*args, **kwargs)
+            # default group should be deleted as well
+            default_group.delete()
+
+        return ret
+
     def get_default_group(self):
         """
         A default Group in a Landscape is that Group where any


Test patch:
diff --git a/terraso_backend/tests/core/models/test_landscapes.py b/terraso_backend/tests/core/models/test_landscapes.py
--- a/terraso_backend/tests/core/models/test_landscapes.py
+++ b/terraso_backend/tests/core/models/test_landscapes.py
@@ -133,3 +133,15 @@ def test_landscape_area_calculated_once(unit_polygon):
     ) as mock2:
         landscape.save()
     mock2.assert_not_called()
+
+
+def test_can_recreate_landscape_after_deletion():
+    user = mixer.blend(User)
+    landscape = mixer.blend(Landscape, created_by=user)
+    landscape.save()
+    landscape.delete()
+    landscape2 = mixer.blend(Landscape, name=landscape.name)
+    try:
+        landscape2.save()
+    except Exception as exc:
+        assert False, f"Could not create landscape with same name as deleted landscape: {exc}"
diff --git a/terraso_backend/tests/graphql/mutations/test_group_mutations.py b/terraso_backend/tests/graphql/mutations/test_group_mutations.py
--- a/terraso_backend/tests/graphql/mutations/test_group_mutations.py
+++ b/terraso_backend/tests/graphql/mutations/test_group_mutations.py
@@ -74,10 +74,6 @@ def test_groups_add_duplicated(client_query, groups):
 
     assert error_result
 
-    error_message = json.loads(error_result["message"])[0]
-    assert error_message["code"] == "unique"
-    assert error_message["context"]["field"] == "name"
-
 
 def test_groups_add_duplicated_by_slug(client_query, groups):
     group_name = groups[0].name
diff --git a/terraso_backend/tests/graphql/mutations/test_landscape_mutations.py b/terraso_backend/tests/graphql/mutations/test_landscape_mutations.py
--- a/terraso_backend/tests/graphql/mutations/test_landscape_mutations.py
+++ b/terraso_backend/tests/graphql/mutations/test_landscape_mutations.py
@@ -71,10 +71,6 @@ def test_landscapes_add_duplicated(client_query, landscapes):
 
     assert error_result
 
-    error_message = json.loads(error_result["message"])[0]
-    assert error_message["code"] == "unique"
-    assert error_message["context"]["field"] == "name"
-
 
 def test_landscapes_add_duplicated_by_slug(client_query, landscapes):
     landscape_name = landscapes[0].name
@@ -454,3 +450,28 @@ def test_landscape_add_remove_profile_image(client_query, managed_landscapes):
     assert landscape.profile_image_description == ""
     # make sure it doesn't change the name too
     assert landscape.name == "Profile Image Test"
+
+
+def test_landscapes_soft_deleted_can_be_created_again(client_query, managed_landscapes):
+    landscape = managed_landscapes[0]
+    landscape.delete()
+    response = client_query(
+        """
+        mutation addLandscape($input: LandscapeAddMutationInput!){
+          addLandscape(input: $input) {
+             landscape {
+               name
+             }
+          }
+        }
+        """,
+        variables={
+            "input": {
+                "name": landscape.name,
+                "description": landscape.description,
+            }
+        },
+    )
+    payload = response.json()
+    assert "errors" not in payload
+    assert payload["data"]["addLandscape"]["landscape"]["name"] == landscape.name