Skip to content

[Fix #3182] Better user deletion #3214

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 6, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions readthedocs/core/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@
import logging

from corsheaders import signals
from django.contrib.auth import get_user_model
from django.db.models.signals import pre_delete
from django.dispatch import Signal
from django.db.models import Q
from django.db.models import Q, Count
from django.dispatch import receiver
from future.backports.urllib.parse import urlparse

from readthedocs.oauth.tasks import bulk_delete_oauth_remote_organizations
from readthedocs.projects.models import Project, Domain

from readthedocs.projects.tasks import bulk_delete_projects

log = logging.getLogger(__name__)
User = get_user_model()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for using get_user_model() here? The pattern we've used throughout the codebase is to import from django.contrib.auth.models import User

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have gone through the documentation and it seems that using settings.AUTH_USER_MODEL is conventional way


WHITELIST_URLS = ['/api/v2/footer_html', '/api/v2/search', '/api/v2/docsearch']

Expand Down Expand Up @@ -62,4 +67,23 @@ def decide_if_cors(sender, request, **kwargs): # pylint: disable=unused-argumen

return False


@receiver(pre_delete, sender=User)
def delete_projects_and_organizations(instance, *args, **kwargs):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function arguments are wrong here. The first argument is sender, or the sending class (User in this case). I believe kwargs will have instance -- that is:

@receiver(pre_delete, sender=User)
def delete_projects_and_organizations(sender, *args, **kwargs):
    instance = kwargs.pop('instance')

https://docs.djangoproject.com/en/1.9/ref/signals/#pre-delete

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! Sorry, I missed the sender part. I will add it in next commit

# Here we count the owner list from the projects that the user own
# Then exclude the projects where there are more than one owner
projects_id = (instance.projects.all().annotate(num_users=Count('users'))
.exclude(num_users__gt=1)
.values_list('id', flat=True))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll have to think more on this logic, but at first glance this will cause problems with our hosting at readthedocs.com, as users don't own projects, a more top-level object, Organizations do. Projects shouldn't be automatically removed like this in that case.


# Here we count the users list from the organization that the user belong
# Then exclude the organizations where there are more than one user
oauth_organizations_id = (instance.oauth_organizations.annotate(num_users=Count('users'))
.exclude(num_users__gt=1)
.values_list('id', flat=True))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OAuth organizations aren't currently shared, so User.oauth_organizations.delete() should be fine. We'll need tests here to ensure this is the case.

Copy link
Member Author

@safwanrahman safwanrahman Oct 31, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont know whats the case actually. maybe it was m2m to give a organization user same permission? to docs


bulk_delete_projects.delay(projects_id)
bulk_delete_oauth_remote_organizations.delay(oauth_organizations_id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is no strong reason that either of these need to be executed via a task, we can just remove them directly here.



signals.check_request_enabled.connect(decide_if_cors)
6 changes: 6 additions & 0 deletions readthedocs/oauth/tasks.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
"""Tasks for OAuth services"""

from __future__ import absolute_import
from celery import task
from django.contrib.auth.models import User
from djcelery import celery as celery_app

from readthedocs.core.utils.tasks import PublicTask
from readthedocs.core.utils.tasks import permission_check
from readthedocs.core.utils.tasks import user_id_matches
from readthedocs.oauth.models import RemoteOrganization
from .services import registry


Expand All @@ -21,5 +23,9 @@ def run_public(self, user_id):
for service in service_cls.for_user(user):
service.sync()

@task()
def bulk_delete_oauth_remote_organizations(organizations_id):
RemoteOrganization.objects.filter(id__in=organizations_id).delete()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like something that should cascade on the database



sync_remote_repositories = celery_app.tasks[SyncRemoteRepositories.name]
5 changes: 5 additions & 0 deletions readthedocs/projects/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -984,3 +984,8 @@ def clear_html_artifacts(version):
if isinstance(version, int):
version = Version.objects.get(pk=version)
remove_dir(version.project.rtd_build_path(version=version.slug))


@task()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without specifying the queue on this task, it will execute on the build instances, which have no access to the database. Specify @task(queue='web') to ensure tasks are executed on the right instances.

def bulk_delete_projects(projects_id):
Project.objects.filter(id__in=projects_id).delete()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason this needs to be executed via a task?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hoping to delete the project directory with the project removal. So was thinking to do it with task. I think we can exclude it from the task

2 changes: 2 additions & 0 deletions readthedocs/templates/profiles/private/delete_account.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
<form method="POST" action=".">
{% csrf_token %}
{{ form }}
<br>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Styling should be handled with css, avoid <br> tags entirely.

<strong>Be careful! This can not be undone!</strong>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Untranslated string here.

<input type="submit" name="submit" value="{% trans "Delete Account" %}" id="submit"/>
</form>
{% endblock %}