Repository: pypi/warehouse

Issue:
Pypi changes feed not reporting a deleted package when a user is deleted
**Describe the bug**
When a user is deleted, all of the packages that they own are deleted as well. The problem is that, when this occurs, the action is not fully reported to the RPC changes feed. Instead, the changes feed will report that a user is "nuked" but it does not report that their packages were deleted. This creates a problem for anyone monitoring the changes feed because they will not receive notifications that a package is removed. 

**Expected behavior**
The expected behavior for this would be that when a user is deleted, and their packages are deleted, you will see in the changes feed that each project is removed.

**To Reproduce**
In order to reproduce this, you will need to create a user that owns a package, and then nuke the user (I assume that can only be done by an admin). Then you can check the RPC changes feed to see that there is a nuke user event, but not a delete project event.

From my client I am requesting the changelog as follows
```
import xmlrpc2
import sys
from pprint import pprint

rpc = xmlrpc2.client.Client(uri="https://pypi.org")

changelog = rpc.changelog_since_serial(int(sys.argv[1]))

pprint(changelog)
```
**My Platform**
Centos6
Python3.6.3

**Additional context**
In correspondence with @ewdurbin, the following was suggested

The admin view for removing a user (“nuke user”) directly deletes project objects from the DB: https://github.com/pypa/warehouse/blob/192e60955051f8ffb34f6cc1f1e3f226acb1b5fb/warehouse/admin/views/users.py#L187

It should _really_ be using the remove project utility: https://github.com/pypa/warehouse/blob/192e60955051f8ffb34f6cc1f1e3f226acb1b5fb/warehouse/utils/project.py#L46



Discussion:

Resolving patch:
diff --git a/warehouse/admin/views/users.py b/warehouse/admin/views/users.py
--- a/warehouse/admin/views/users.py
+++ b/warehouse/admin/views/users.py
@@ -184,6 +184,15 @@ def user_delete(request):
             .subquery()
         )
     )
+    for project in projects:
+        request.db.add(
+            JournalEntry(
+                name=project.name,
+                action="remove project",
+                submitted_by=request.user,
+                submitted_from=request.remote_addr,
+            )
+        )
     projects.delete(synchronize_session=False)
 
     # Update all journals to point to `deleted-user` instead


Test patch:
diff --git a/tests/unit/admin/views/test_users.py b/tests/unit/admin/views/test_users.py
--- a/tests/unit/admin/views/test_users.py
+++ b/tests/unit/admin/views/test_users.py
@@ -16,12 +16,13 @@
 import pytest
 
 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound
+from sqlalchemy.orm import joinedload
 from webob.multidict import MultiDict, NoVars
 
 from warehouse.accounts.interfaces import IUserService
 from warehouse.accounts.models import DisableReason
 from warehouse.admin.views import users as views
-from warehouse.packaging.models import Project
+from warehouse.packaging.models import JournalEntry, Project
 
 from ....common.db.accounts import EmailFactory, User, UserFactory
 from ....common.db.packaging import JournalEntryFactory, ProjectFactory, RoleFactory
@@ -200,10 +201,13 @@ def test_deletes_user(self, db_request, monkeypatch):
         user = UserFactory.create()
         project = ProjectFactory.create()
         another_project = ProjectFactory.create()
-        journal = JournalEntryFactory(submitted_by=user)
         RoleFactory(project=project, user=user, role_name="Owner")
         deleted_user = UserFactory.create(username="deleted-user")
 
+        # Create an extra JournalEntry by this user which should be
+        # updated with the deleted-user user.
+        JournalEntryFactory(submitted_by=user, action="some old journal")
+
         db_request.matchdict["user_id"] = str(user.id)
         db_request.params = {"username": user.username}
         db_request.route_path = pretend.call_recorder(lambda a: "/foobar")
@@ -219,7 +223,27 @@ def test_deletes_user(self, db_request, monkeypatch):
         assert db_request.route_path.calls == [pretend.call("admin.user.list")]
         assert result.status_code == 303
         assert result.location == "/foobar"
-        assert journal.submitted_by == deleted_user
+
+        # Check that the correct journals were written/modified
+        old_journal = (
+            db_request.db.query(JournalEntry)
+            .options(joinedload(JournalEntry.submitted_by))
+            .filter(JournalEntry.action == "some old journal")
+            .one()
+        )
+        assert old_journal.submitted_by == deleted_user
+        remove_journal = (
+            db_request.db.query(JournalEntry)
+            .filter(JournalEntry.action == "remove project")
+            .one()
+        )
+        assert remove_journal.name == project.name
+        nuke_journal = (
+            db_request.db.query(JournalEntry)
+            .filter(JournalEntry.action == "nuke user")
+            .one()
+        )
+        assert nuke_journal.name == f"user:{user.username}"
 
     def test_deletes_user_bad_confirm(self, db_request, monkeypatch):
         user = UserFactory.create()