diff --git a/readthedocs/core/signals.py b/readthedocs/core/signals.py index a4f029c6b3e..4443fa060bd 100644 --- a/readthedocs/core/signals.py +++ b/readthedocs/core/signals.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Signal handling for core app.""" import logging @@ -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__) @@ -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() diff --git a/readthedocs/profiles/views.py b/readthedocs/profiles/views.py index 4a300e09257..2e7b26eaf06 100644 --- a/readthedocs/profiles/views.py +++ b/readthedocs/profiles/views.py @@ -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): @@ -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' @@ -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 diff --git a/readthedocs/projects/templatetags/projects_tags.py b/readthedocs/projects/templatetags/projects_tags.py index 4fbf769b655..a66b61b1969 100644 --- a/readthedocs/projects/templatetags/projects_tags.py +++ b/readthedocs/projects/templatetags/projects_tags.py @@ -4,7 +4,6 @@ from readthedocs.projects.version_handling import comparable_version - register = template.Library() diff --git a/readthedocs/projects/tests/test_utils.py b/readthedocs/projects/tests/test_utils.py new file mode 100644 index 00000000000..cc6281bee9c --- /dev/null +++ b/readthedocs/projects/tests/test_utils.py @@ -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))) diff --git a/readthedocs/projects/utils.py b/readthedocs/projects/utils.py index a4821d9ba94..df03696277d 100644 --- a/readthedocs/projects/utils.py +++ b/readthedocs/projects/utils.py @@ -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__) @@ -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) + ) diff --git a/readthedocs/templates/profiles/private/delete_account.html b/readthedocs/templates/profiles/private/delete_account.html index 567eff17cfb..4b451949abb 100644 --- a/readthedocs/templates/profiles/private/delete_account.html +++ b/readthedocs/templates/profiles/private/delete_account.html @@ -9,6 +9,24 @@ {% block edit_content_header %} {% trans "Delete Account" %} {% endblock %} {% block edit_content %} + + {% block delete-warning %} +

+

+ All projects where you are the only owner will be deleted. + If you want to keep a project, add another owner or transfer ownership. + The following projects and all their documentation will be deleted. +

+ +

+ {% endblock%} +
{% csrf_token %} {{ form }} @@ -16,5 +34,6 @@ {% trans "Be careful! This can not be undone!" %} -
+ + {% endblock %}