-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
[Fix #4576] Do not delete projects which have multiple users #4577
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ | |
from django.dispatch import receiver | ||
from future.backports.urllib.parse import urlparse | ||
|
||
from readthedocs.oauth.models import RemoteOrganization | ||
from readthedocs.projects.models import Project, Domain | ||
|
||
log = logging.getLogger(__name__) | ||
|
@@ -79,12 +80,17 @@ def decide_if_cors(sender, request, **kwargs): # pylint: disable=unused-argumen | |
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 | ||
projects = instance.projects.all().annotate(num_users=Count('users')).exclude(num_users__gt=1) | ||
# 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)) | ||
|
||
# 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 = (instance.oauth_organizations.annotate(num_users=Count('users')) | ||
.exclude(num_users__gt=1)) | ||
oauth_organizations = (RemoteOrganization.objects.annotate(num_users=Count('users')) | ||
.filter(users=instance.id) | ||
.exclude(num_users__gt=1)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These changes should conform to our styleguide by running There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it does not break PEP8 styleguide. Can you explain how it would change? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps a link to the style guide is warranted. I don't know of any RTD style guide outside of what the tests do. I searched our docs but I came up empty. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have the style guide inside pre-commit p: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup, no explicit styleguide. it's all enforced by pre-commit via yapf and friends. In this specific case, we opt for no right side alignment of variables and arguments. Both are pep8 compliant, but right side alignment is too unreadable and annoying to maintain. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Info on pre-commit procedure is here: |
||
|
||
projects.delete() | ||
oauth_organizations.delete() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import pytest | ||
import django_dynamic_fixture | ||
|
||
from django.contrib.auth.models import User | ||
|
||
from readthedocs.oauth.models import RemoteOrganization | ||
from readthedocs.projects.models import Project | ||
|
||
|
||
@pytest.mark.django_db | ||
class TestProjectOrganizationSignal(object): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm mostly certain we have tests for both of these cases already. Can you provide test output of these tests without your patch applied? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I missed adding tests while implementing the feature for both of the case. =============================================================================== FAILURES ===============================================================================
______________________________________ TestProjectOrganizationSignal.test_multiple_users_project_organization_not_delete[Project] ______________________________________
self = <readthedocs.core.tests.test_signals.TestProjectOrganizationSignal object at 0x10eb85d30>, model_class = <class 'readthedocs.projects.models.Project'>
@pytest.mark.parametrize('model_class', [Project, RemoteOrganization])
def test_multiple_users_project_organization_not_delete(self, model_class):
"""
Check Project or RemoteOrganization which have multiple users do not get deleted
when any of the user delete his account.
"""
obj = django_dynamic_fixture.get(model_class)
user1 = django_dynamic_fixture.get(User)
user2 = django_dynamic_fixture.get(User)
obj.users.add(user1, user2)
obj.refresh_from_db()
assert obj.users.all().count() > 1
# Delete 1 user of the project
user1.delete()
# The project should still exist and it should have 1 user
> obj.refresh_from_db()
core/tests/test_signals.py:52:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../.tox/py36/lib/python3.6/site-packages/django/db/models/base.py:592: in refresh_from_db
db_instance = db_instance_qs.get()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = [], args = (), kwargs = {}, clone = [], num = 0
def get(self, *args, **kwargs):
"""
Performs the query and returns a single object matching the given
keyword arguments.
"""
clone = self.filter(*args, **kwargs)
if self.query.can_filter() and not self.query.distinct_fields:
clone = clone.order_by()
num = len(clone)
if num == 1:
return clone._result_cache[0]
if not num:
raise self.model.DoesNotExist(
"%s matching query does not exist." %
> self.model._meta.object_name
)
E readthedocs.projects.models.DoesNotExist: Project matching query does not exist.
../.tox/py36/lib/python3.6/site-packages/django/db/models/query.py:387: DoesNotExist
________________________________ TestProjectOrganizationSignal.test_multiple_users_project_organization_not_delete[RemoteOrganization] _________________________________
self = <readthedocs.core.tests.test_signals.TestProjectOrganizationSignal object at 0x10eb9f9b0>, model_class = <class 'readthedocs.oauth.models.RemoteOrganization'>
@pytest.mark.parametrize('model_class', [Project, RemoteOrganization])
def test_multiple_users_project_organization_not_delete(self, model_class):
"""
Check Project or RemoteOrganization which have multiple users do not get deleted
when any of the user delete his account.
"""
obj = django_dynamic_fixture.get(model_class)
user1 = django_dynamic_fixture.get(User)
user2 = django_dynamic_fixture.get(User)
obj.users.add(user1, user2)
obj.refresh_from_db()
assert obj.users.all().count() > 1
# Delete 1 user of the project
user1.delete()
# The project should still exist and it should have 1 user
> obj.refresh_from_db()
core/tests/test_signals.py:52:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../.tox/py36/lib/python3.6/site-packages/django/db/models/base.py:592: in refresh_from_db
db_instance = db_instance_qs.get()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = [], args = (), kwargs = {}, clone = [], num = 0
def get(self, *args, **kwargs):
"""
Performs the query and returns a single object matching the given
keyword arguments.
"""
clone = self.filter(*args, **kwargs)
if self.query.can_filter() and not self.query.distinct_fields:
clone = clone.order_by()
num = len(clone)
if num == 1:
return clone._result_cache[0]
if not num:
raise self.model.DoesNotExist(
"%s matching query does not exist." %
> self.model._meta.object_name
)
E readthedocs.oauth.models.DoesNotExist: RemoteOrganization matching query does not exist.
../.tox/py36/lib/python3.6/site-packages/django/db/models/query.py:387: DoesNotExist
================================================================= 2 failed, 2 passed in 12.99 seconds ==================================================================
ERROR: InvocationError for command '/Users/safwan/readthedocs/.tox/py36/bin/py.test core -s --reuse-db -vvv' (exited with code 1) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe is worth it to add a test for the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I am going to add some test on that! but its out of scope of this PR! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great, test output looks good. That's what I'd expect if the delete cascaded to the project |
||
|
||
@pytest.mark.parametrize('model_class', [Project, RemoteOrganization]) | ||
def test_project_organization_get_deleted_upon_user_delete(self, model_class): | ||
""" | ||
If the user has Project or RemoteOrganization where he is the only user, | ||
upon deleting his account, the Project or RemoteOrganization should also get | ||
deleted. | ||
""" | ||
|
||
obj = django_dynamic_fixture.get(model_class) | ||
user1 = django_dynamic_fixture.get(User) | ||
obj.users.add(user1) | ||
|
||
obj.refresh_from_db() | ||
assert obj.users.all().count() == 1 | ||
|
||
# Delete the user | ||
user1.delete() | ||
# The object should not exist | ||
obj = model_class.objects.all().filter(id=obj.id) | ||
assert not obj.exists() | ||
|
||
@pytest.mark.parametrize('model_class', [Project, RemoteOrganization]) | ||
def test_multiple_users_project_organization_not_delete(self, model_class): | ||
""" | ||
Check Project or RemoteOrganization which have multiple users do not get deleted | ||
when any of the user delete his account. | ||
""" | ||
|
||
obj = django_dynamic_fixture.get(model_class) | ||
user1 = django_dynamic_fixture.get(User) | ||
user2 = django_dynamic_fixture.get(User) | ||
obj.users.add(user1, user2) | ||
|
||
obj.refresh_from_db() | ||
assert obj.users.all().count() > 1 | ||
# Delete 1 user of the project | ||
user1.delete() | ||
|
||
# The project should still exist and it should have 1 user | ||
obj.refresh_from_db() | ||
assert obj.id | ||
obj_users = obj.users.all() | ||
assert len(obj_users) == 1 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to take the queryset value in memory eventually. So better to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wouldn't use |
||
assert user2 in obj_users |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ran this query and it seems correct to me.