Repository: CiviWiki/OpenCiviWiki

Issue:
Account Deletion.
There seems to be no way to delete accounts as yet

## Task

- [ ] sketch and discuss the "delete my account" user interface design
- [ ] implement the design as an HTML template
- [ ] create a Django view to render the HTML template
- [ ] define a `POST` handler for the Django view that soft-deletes the user account
- [ ] ensure that users can only delete their own profile
- [ ] ensure that the user object remains but personally identifiable information (PII) is expunged


Discussion:
This is a good feature request. We will need to design the account deletion process, including considerations about how to handle user-created data, if any has been created by the account.
@GokuMizuno Didn't you fix this in the GDPR update? 
Hi, is this issue still relevant, I would like to work on this.  
We are happy for your help. I believe it is still relevant but @Brylie
Christopher Oxley <brylie@amble.fi> probably knows better.



<https://www.avast.com/sig-email?utm_medium=email&utm_source=link&utm_campaign=sig-email&utm_content=webmail&utm_term=icon>
Virus-free.
www.avast.com
<https://www.avast.com/sig-email?utm_medium=email&utm_source=link&utm_campaign=sig-email&utm_content=webmail&utm_term=link>
<#DAB4FAD8-2DD7-40BB-A1B8-4E2AA1F9FDF2>

On Sat, Mar 2, 2019 at 8:52 AM Wang Junming <notifications@github.com>
wrote:

> Hi, is this issue still relevant, I would like to work on this.
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <https://github.com/CiviWiki/OpenCiviWiki/issues/227#issuecomment-468937891>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AdJ-qp9biCT_5cVpEf66pp9LWb202v2tks5vSqxbgaJpZM4YIZf2>
> .
>


-- 
Mitchell D. West
Founder and Executive Director of CiviWiki.org

Thanks @junming403. Do you already have a development environment set up?
@brylie Hi sir, Yes I have the environment set up. Can you tell me more about this feature?
Basically, we want to give users a way to remove their personal account. There should be a button on their profile page that will perform a cascsding delete from their user object. Care should be taken to consider the impact on shared content, such as Civis.

Edit: we no longer want a cascading delete. Rather, we want a soft-delete that only removes personally identifiable information (PII) while keeping other related data in tact.
Thank you sir, I will start to work on this issue, will create a PR once it's done
You might also check Gordon strangler's pr's. I think he put together some
code on this pr. But I understand if it's easier to start from scratch. And
modular solutions are preferred. Thank you for the help!



On Mon, Mar 4, 2019, 6:19 AM Wang Junming <notifications@github.com> wrote:

> Thank you sir, I will start to work on this issue, will create a PR once
> it's done
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <https://github.com/CiviWiki/OpenCiviWiki/issues/227#issuecomment-469268647>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AdJ-qvS-WUId3Jj63nGnOM3ukFgogDySks5vTStogaJpZM4YIZf2>
> .
>

Hi can I clarify on the requirement? 

When the account is deleted, the followings are also deleted:
1. The account itself.
2. Civis created by the account.
3. Threads created by the account.
4. Civis related to those threads created by the account.
5. Activities of the account or deleted threads
6. Notifications of the account or deleted threads
7. Rebuttal of the account
8. Responses of those deleted rebuttal
9. Responses of the account
10. Invitations with the account as host user.

Please correct me if it is wrong.

BTW: Shall we perform soft-delete or real delete?

From my perspective, we should only delete the account itself. All of the other conversation is a matter of public record. E.g. when you delete a Hacker News or Reddit account, your comments are still visible to preserve the context.

The thing we are concerned about primarily is that people's personally identifiable details are deleted. A cascading delete of all related content would be quite disruptive and non-trivial.
Note: comments, civis, rebuttals, etc. where the `body` text contains personally identifiable material can be treated as an edge case that can be cleaned up by manual moderation.

Just indicate that the author account was **deleted** in the UI, while preserving the rest of the content, connections to other Civis, etc.
@junming403 how are you progressing with this issue?
@brylie 
We can just remove the username and replace it with a random username and random email and then disable the account.
Also, we can create a user that is reserved for just deleted accounts.
Let me know your thoughts on this one
@max1204 I like your ideas. Would you mind sketching the "delete my account" page template and sharing the sketch in this discussion?
@max1204 I've added a task list to the issue description based on your idea.
@brylie I was thinking about the design do we really need the template for deletion. We could just merge this into the settings or profile page. 

The only need I see is when we need to show the user what information will be deleted.
![design](https://user-images.githubusercontent.com/20409353/123630310-01cbd780-d833-11eb-949b-de3a534f9619.jpeg)

@max1204 the button would suffice. Make sure there is a confirmation step that warns the user that they will not be able to log in after the account is deleted and that the account is anonymized instead of fully deleted in order to preserve data integrity.
A couple of concerns:

1. What happens to the content created by the user deleting their account?
2. If the content is being retained, how is it being anonymised? How are links changed? Suppose I created an account and shared the link for one of my posts, then I delete my accounts. But the link I posted might still be associated with me through search engines and social media. Such links need to be replaced with new ones to make "right to forget" a reality.

The content should remain in-tact for now, until we think of a more comprehensive strategy. We are just expunging the user name, by replacing fields with text like "account deleted." That way, the content should still function correctly. External services are out of our purview, so we are not responsible for "forgetting" the user outside CiviWiki.
Why not just have an account flag isActive, and set it to false when the
account owner deletes it?  Then, when pulling comments in a thread, check
for the flag.  If the flag is false, then make the display namw be
something like [deleted].

In pseudocode

If (!isActive):
    Account.DisplayName = "[deleted]";
Else:
    Account.DisplayName = Account.Name;

Hope this helps.


On Mon, Jun 28, 2021, 09:21 Brylie Christopher Oxley <
***@***.***> wrote:

> The content should remain in-tact for now, until we think of a more
> comprehensive strategy. We are just expunging the user name, by replacing
> fields with text like "account deleted." That way, the content should still
> function correctly. External services are out of our purview, so we are not
> responsible for "forgetting" the user outside CiviWiki.
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/CiviWiki/OpenCiviWiki/issues/227#issuecomment-869679642>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ABUAWBMLWP4CLWLRG6IDGWLTVBZMFANCNFSM4GBBS73A>
> .
>

Part of the idea here is that we need to handle GDPR "[right to be forgotten](https://en.wikipedia.org/wiki/Right_to_be_forgotten)" requests. So, we should consider how to safely expunge the personally identifiable information (PII) from the database as well.
Hi, I want to start my open source contribution journey, I would like to work on this as my first contribution. Can You help me?

@shayan-cyber, thanks for your interest! Please [open a new discussion](https://github.com/CiviWiki/OpenCiviWiki/discussions/new) to introduce yourself and we can assist you with getting your development environment set up.
> @shayan-cyber, thanks for your interest! Please [open a new discussion](https://github.com/CiviWiki/OpenCiviWiki/discussions/new) to introduce yourself and we can assist you with getting your development environment set up.

yeah ,I've created a thread to introduce myself
@brylie Is this issue still being worked upon I have some time this weekend to test and finish this?
Go ahead and take the issue @max1204 
@brylie  Hello!

I found this issue on a Awesome List for Hacktoberfest and I'd like to tackle this issue.

It seems it is a simple "just change the 'on_delete' property to 'SET_NULL'" and change the foreignkeys to 'null=True', which would remove the ID to the original data, but I'm kinda worried about LGPD (Brazilian data protection law) and GDPR, 'cause although the data doesn't point to the original user, it could still identify them. IANAL, but that's what I know about these laws.

I can work on the first part, but the last bit worries me.

> It seems it is a simple "just change the 'on_delete' property to 'SET_NULL'" and change the foreignkeys to 'null=True', which would remove the ID to the original data, but I'm kinda worried about LGPD (Brazilian data protection law) and GDPR, 'cause although the data doesn't point to the original user, it could still identify them

The only place we store personally identifiable information is:

- `User` - via email field
- `Profile` - all fields are PII

So, we need to delete the `User` and `Profile` instances for any given user on request.

Data protection laws give organizations some right to keep data when it is in the interest of the organization. In the case of CiviWiki, the user generated content, such as discussion threads, is important to keep in order to preserve the integrity of the discussion.

Deleting the `User` and `Profile` objects and setting the related fields to `NULL` on user-generated content strikes a good balance for privacy and data integrity while being a task that is easy to achieve.
I would like to participate in this task.
I'm not particularly experienced in UI, but I think I can help with the actual deletion implementation.
Would you be able to clarify about not deleting the object?  
This is what I have so far, but I'm not sure if this is something you are looking for?

![Screen Shot 2021-12-16 at 10 45 20 PM](https://user-images.githubusercontent.com/55517552/146485571-a90321bd-0774-4cbe-9782-8abf9caa8328.png)


One idea is to just expunge the fields with personally identifiable information, such as `first_name`, `last_name`, and `email`.

The basic goal is to preserve the integrity of other content, without a cascading deletion when allowing a user to mark their account as deleted. Another approach would be to use `on_delete=SET_NULL` for any related foreign key relationships.
Hi @mnoguier! How's the issue going?

@brylie I'm not saying which approach is better, I'd just like to point out that one benefit of going with the first option (expunging personally identifiable information) is that it would aid a lot of future tasks. For example if somebody created an account, created a bunch of different civis, then immediately deleted the account. That is some indication of a bad faith actor and if we `on_delete=SET_NULL` then we have no way of investigating things like that. 
Another feature that we might be interested in the future is aggregating user statistics. For example "How interconnected are the topics people care about? Do they cluster? i.e. if somebody cares about one issue, what are the chances they'll care about another one?". That data would be lost if we went with the second approach.
Hi, is this still open for contribution? My friend and I are seniors at the University of Michigan with previous experience in Python. We're trying to get involved in open source projects and thought CiviWiki was pretty cool

Resolving patch:
diff --git a/project/accounts/api.py b/project/accounts/api.py
--- a/project/accounts/api.py
+++ b/project/accounts/api.py
@@ -461,3 +461,41 @@ def edit_user_categories(request):
         return HttpResponseBadRequest(reason=str(e))
     except Exception as e:
         return HttpResponseServerError(reason=str(e))
+
+
+@login_required
+def delete_user(request):
+    """
+    Delete User Information
+    """
+    try:
+        # Get current user
+        user = get_user_model().objects.get(username=request.user.username)
+        profile = Profile.objects.get(user=user)
+        # Expunge personally identifiable data in user obj, feel free to change
+        data = {
+            "is_active": False,
+            "email": "",
+            "first_name": "",
+            "last_name": "",
+            "username": "[Deleted-" + str(user.id) + "]"
+        }
+        user.__dict__.update(data)
+        user.save()  # Update into database
+
+        data = {  # Expunge personally identifiable data in profile obj
+            "first_name": "",
+            "last_name": "",
+            "about_me": ""
+        }
+        profile.__dict__.update(data)
+        profile.save()
+    except get_user_model().DoesNotExist as e:
+        return HttpResponseBadRequest(reason=str(e))
+    except Exception as e:
+        return HttpResponseServerError(reason=str(e))
+
+    user.refresh_from_db()  # Make update viewable
+    profile.refresh_from_db()
+
+    return JsonResponse({"result": "User successfully deleted."})
diff --git a/project/accounts/urls/api.py b/project/accounts/urls/api.py
--- a/project/accounts/urls/api.py
+++ b/project/accounts/urls/api.py
@@ -7,6 +7,7 @@
     path("account_card/<str:username>/", api.get_card, name="get_card"),
     path("feed/", api.get_feed, name="get_feed"),
     path("edituser/", api.edit_user, name="edit_user"),
+    path("deleteuser/", api.delete_user, name="delete_user"),
     path("upload_profile/", api.upload_profile_image, name="upload_profile"),
     path("clear_profile/", api.clear_profile_image, name="clear_profile"),
     path("follow/", api.request_follow, name="follow_user"),


Test patch:
diff --git a/project/accounts/tests/test_api.py b/project/accounts/tests/test_api.py
--- a/project/accounts/tests/test_api.py
+++ b/project/accounts/tests/test_api.py
@@ -235,6 +235,81 @@ def test_first_name_last_name_about_fields_can_be_editable(self):
         self.assertEqual(self.user.profile.about_me, "About me")
 
 
+class DeleteUserTests(BaseTestCase):
+    def test_delete_user_removes_from_database(self):
+        self.delete_dummy = get_user_model().objects.create_user(
+            username="s_delete", email="test@test.com", password="password123",
+            first_name="Bob", last_name="George"
+        )
+        self.assertEqual(self.delete_dummy.first_name, "Bob")
+        self.client.login(username="s_delete", password="password123")
+        self.client.post(reverse("delete_user"))
+        self.delete_dummy.refresh_from_db()
+        assert(self.delete_dummy.username.startswith("[Deleted-"))
+        self.assertEqual(self.delete_dummy.first_name, "")
+        self.assertEqual(self.delete_dummy.last_name, "")
+        self.client.logout()
+
+    def test_delete_user_fails_if_not_logged_in(self):
+        self.delete_dummy = get_user_model().objects.create_user(
+            username="f_delete", email="test@test.com", password="password123",
+            first_name="John", last_name="Smith"
+        )
+        self.assertEqual(self.delete_dummy.first_name, "John")
+        self.client.post(reverse("delete_user"))
+        self.delete_dummy.refresh_from_db()
+        self.assertEqual(self.delete_dummy.username, "f_delete")
+        self.assertEqual(self.delete_dummy.first_name, "John")
+        self.assertEqual(self.delete_dummy.last_name, "Smith")
+
+    def test_delete_user_removes_profile_information(self):
+        self.delete_dummy = get_user_model().objects.create_user(
+            username="prof", email="test@test.com", password="password123",
+            first_name="Aerith", last_name="Sans"
+        )
+        self.client.login(username="prof", password="password123")
+        self.dummy_profile = Profile.objects.get(user=self.delete_dummy)
+        data = {
+            "first_name": "Aerith",
+            "last_name": "Sans",
+            "about_me": "Dummy"
+        }
+        self.dummy_profile.__dict__.update(data)
+
+        self.assertEqual(self.dummy_profile.first_name, "Aerith")
+        self.client.post(reverse("delete_user"))
+        self.dummy_profile.refresh_from_db()
+        self.assertEqual(self.dummy_profile.about_me, "")
+        self.assertEqual(self.dummy_profile.first_name, "")
+        self.assertEqual(self.dummy_profile.last_name, "")
+        self.client.logout()
+
+    def test_delete_user_can_delete_reused_usernames(self):
+        self.delete_dummy = get_user_model().objects.create_user(
+            username="imposter", email="test@test.com", password="password123",
+            first_name="Among", last_name="Us"
+        )
+        self.client.login(username="imposter", password="password123")
+        self.assertEqual(self.delete_dummy.first_name, "Among")
+        self.client.post(reverse("delete_user"))
+        self.delete_dummy.refresh_from_db()
+        self.client.logout()
+
+        self.second_dummy = get_user_model().objects.create_user(
+            username="imposter", email="test@test.com", password="password123",
+            first_name="Tyler", last_name="One"
+        )
+        self.client.login(username="imposter", password="password123")
+        self.assertEqual(self.second_dummy.username, "imposter")
+        self.assertEqual(self.second_dummy.first_name, "Tyler")
+        self.client.post(reverse("delete_user"))
+        self.second_dummy.refresh_from_db()
+        assert(self.second_dummy.username.startswith("[Deleted-"))
+        self.assertEqual(self.second_dummy.first_name, "")
+        self.assertEqual(self.second_dummy.last_name, "")
+        self.client.logout()
+
+
 class UploadProfileImage(BaseTestCase):
     """A class to test upload_profile_image function"""