Skip to content

Warn about projects to be deleted whe an account is deleted #6414

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

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
22 changes: 6 additions & 16 deletions readthedocs/core/signals.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-

"""Signal handling for core app."""

import logging
Expand All @@ -14,7 +12,7 @@

from readthedocs.oauth.models import RemoteOrganization
from readthedocs.projects.models import Domain, Project

from readthedocs.projects.utils import get_projects_only_owner

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -84,23 +82,15 @@ def decide_if_cors(sender, request, **kwargs): # pylint: disable=unused-argumen

@receiver(pre_delete, sender=settings.AUTH_USER_MODEL)
def delete_projects_and_organizations(sender, instance, *args, **kwargs):
# Here we count the owner list from the projects that the user own
# Then exclude the projects where there are more than one owner
# Add annotate before filter
# https://github.com/rtfd/readthedocs.org/pull/4577
# https://docs.djangoproject.com/en/2.1/topics/db/aggregation/#order-of-annotate-and-filter-clauses # noqa
projects = (
Project.objects.annotate(num_users=Count('users')
).filter(users=instance.id
).exclude(num_users__gt=1)
)
user = instance
projects = get_projects_only_owner(user)

# 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 = (
RemoteOrganization.objects.annotate(num_users=Count('users')
).filter(users=instance.id
).exclude(num_users__gt=1)
RemoteOrganization.objects
.annotate(num_users=Count('users'))
.filter(users=instance.id, num_users=1)
)

projects.delete()
Expand Down
19 changes: 18 additions & 1 deletion readthedocs/profiles/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from readthedocs.core.mixins import PrivateViewMixin
from readthedocs.core.models import UserProfile
from readthedocs.core.utils.extend import SettingsOverrideObject
from readthedocs.projects.utils import get_projects_only_owner


class ProfileEdit(PrivateViewMixin, UpdateView):
Expand All @@ -46,7 +47,7 @@ def get_success_url(self):
)


class AccountDelete(PrivateViewMixin, SuccessMessageMixin, FormView):
class AccountDeleteBase(PrivateViewMixin, SuccessMessageMixin, FormView):

form_class = UserDeleteForm
template_name = 'profiles/private/delete_account.html'
Expand All @@ -64,10 +65,26 @@ def get_form(self, data=None, files=None, **kwargs):
kwargs['instance'] = self.get_object()
return super().get_form(data, files, **kwargs)

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update(self.get_objects_to_be_deleted())
return context

def get_objects_to_be_deleted(self):
"""Return an additional context with objects to be deleted to show in the template."""
return {
'projects_to_be_deleted': get_projects_only_owner(self.request.user),
}

def get_success_url(self):
return reverse('homepage')


class AccountDelete(SettingsOverrideObject):

_default_class = AccountDeleteBase


class ProfileDetailBase(DetailView):

model = User
Expand Down
1 change: 0 additions & 1 deletion readthedocs/projects/templatetags/projects_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from readthedocs.projects.version_handling import comparable_version


register = template.Library()


Expand Down
48 changes: 48 additions & 0 deletions readthedocs/projects/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from django.contrib.auth.models import User
from django.test import TestCase
from django_dynamic_fixture import get

from readthedocs.projects.models import Project
from readthedocs.projects.utils import get_projects_only_owner


class TestUtils(TestCase):

def test_get_projects_only_owner(self):
user = get(User)
another_user = get(User)

project_one = get(
Project,
slug='one',
users=[user],
main_language_project=None,
)
project_two = get(
Project,
slug='two',
users=[user],
main_language_project=None,
)
project_three = get(
Project,
slug='three',
users=[another_user],
main_language_project=None,
)
project_four = get(
Project,
slug='four',
users=[user, another_user],
main_language_project=None,
)

project_five = get(
Project,
slug='five',
users=[],
main_language_project=None,
)

expected = {project_one, project_two}
self.assertEqual(expected, set(get_projects_only_owner(user)))
13 changes: 11 additions & 2 deletions readthedocs/projects/utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
"""Utility functions used by projects."""

import logging
import os

from django.conf import settings

from django.db.models import Count

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -56,3 +55,13 @@ class Echo:
def write(self, value):
"""Write the value by returning it, instead of storing in a buffer."""
return value


def get_projects_only_owner(user):
"""Get projects where `user` is the only owner."""
from readthedocs.projects.models import Project
return (
Project.objects
.annotate(num_users=Count('users'))
.filter(users=user.id, num_users=1)
)
21 changes: 20 additions & 1 deletion readthedocs/templates/profiles/private/delete_account.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,31 @@
{% block edit_content_header %} {% trans "Delete Account" %} {% endblock %}

{% block edit_content %}

{% block delete-warning %}
<p>
<p>
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't believe nested <p> is valid, and shouldn't be required either way.

<strong>All projects where you are the only owner will be deleted.</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. Also is this the messaging we'll give to commercial users?

If you want to keep a project, add another owner or transfer ownership.
Copy link
Member

Choose a reason for hiding this comment

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

I think we could add a subtitle here. What about something like this?


Delete Account

All projects where you are the only owner will be deleted.
If you want to keep a project, add another owner or transfer ownership.

Projects to be delete

The following projects and all their documentation will be deleted:

  • Project A
  • Project B

Copy link
Contributor

Choose a reason for hiding this comment

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

👍 on the heading, we can skip the paragraph below the heading which is redundant.

The following projects and all their documentation will be deleted.
</p>
<ul>
{% for project in projects_to_be_deleted %}
<li>
<a href="{{ project.get_absolute_url }}">{{ project.slug }}</a>
</li>
{% endfor %}
</ul>
</p>
{% endblock%}

<form method="POST" action=".">
{% csrf_token %}
{{ form }}
<div>
<strong>{% trans "Be careful! This can not be undone!" %}</strong>
</div>
<input type="submit" name="submit" value="{% trans "Delete Account" %}" id="submit"/>
</form>
</form>

{% endblock %}