Skip to content

Commit ddb20a7

Browse files
committed
Merge remote-tracking branch 'origin/remote-repository-normalization' into rel
2 parents c7e3712 + 73c0447 commit ddb20a7

26 files changed

+947
-309
lines changed

readthedocs/api/v2/serializers.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ class RemoteOrganizationSerializer(serializers.ModelSerializer):
177177

178178
class Meta:
179179
model = RemoteOrganization
180-
exclude = ('json', 'email', 'users')
180+
exclude = ('email', 'users',)
181181

182182

183183
class RemoteRepositorySerializer(serializers.ModelSerializer):
@@ -188,16 +188,30 @@ class RemoteRepositorySerializer(serializers.ModelSerializer):
188188

189189
# This field does create an additional query per object returned
190190
matches = serializers.SerializerMethodField()
191+
admin = serializers.SerializerMethodField('is_admin')
191192

192193
class Meta:
193194
model = RemoteRepository
194-
exclude = ('json', 'users')
195+
exclude = ('users',)
195196

196197
def get_matches(self, obj):
197198
request = self.context['request']
198199
if request.user is not None and request.user.is_authenticated:
199200
return obj.matches(request.user)
200201

202+
def is_admin(self, obj):
203+
request = self.context['request']
204+
205+
# Use annotated value from RemoteRepositoryViewSet queryset
206+
if hasattr(obj, 'admin'):
207+
return obj.admin
208+
209+
if request.user and request.user.is_authenticated:
210+
return obj.remote_repository_relations.filter(
211+
user=request.user, admin=True
212+
).exists()
213+
return False
214+
201215

202216
class ProviderSerializer(serializers.Serializer):
203217

readthedocs/api/v2/views/model_views.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from allauth.socialaccount.models import SocialAccount
77
from django.conf import settings
8+
from django.db.models import BooleanField, Case, Value, When
89
from django.shortcuts import get_object_or_404
910
from django.template.loader import render_to_string
1011
from rest_framework import decorators, permissions, status, viewsets
@@ -20,7 +21,11 @@
2021
from readthedocs.projects.models import Domain, Project
2122
from readthedocs.storage import build_commands_storage
2223

23-
from ..permissions import APIPermission, APIRestrictedPermission, IsOwner
24+
from ..permissions import (
25+
APIPermission,
26+
APIRestrictedPermission,
27+
IsOwner,
28+
)
2429
from ..serializers import (
2530
BuildAdminSerializer,
2631
BuildCommandSerializer,
@@ -298,10 +303,10 @@ class RemoteOrganizationViewSet(viewsets.ReadOnlyModelViewSet):
298303
def get_queryset(self):
299304
return (
300305
self.model.objects.api(self.request.user).filter(
301-
account__provider__in=[
306+
remote_organization_relations__account__provider__in=[
302307
service.adapter.provider_id for service in registry
303-
],
304-
)
308+
]
309+
).distinct()
305310
)
306311

307312

@@ -313,7 +318,21 @@ class RemoteRepositoryViewSet(viewsets.ReadOnlyModelViewSet):
313318
pagination_class = RemoteProjectPagination
314319

315320
def get_queryset(self):
316-
query = self.model.objects.api(self.request.user)
321+
if not self.request.user.is_authenticated:
322+
return self.model.objects.none()
323+
324+
# TODO: Optimize this query after deployment
325+
query = self.model.objects.api(self.request.user).annotate(
326+
admin=Case(
327+
When(
328+
remote_repository_relations__user=self.request.user,
329+
remote_repository_relations__admin=True,
330+
then=Value(True)
331+
),
332+
default=Value(False),
333+
output_field=BooleanField()
334+
)
335+
)
317336
full_name = self.request.query_params.get('full_name')
318337
if full_name is not None:
319338
query = query.filter(full_name__icontains=full_name)
@@ -324,18 +343,20 @@ def get_queryset(self):
324343
own = self.request.query_params.get('own', None)
325344
if own is not None:
326345
query = query.filter(
327-
account__provider=own,
346+
remote_repository_relations__account__provider=own,
328347
organization=None,
329348
)
330349

331350
query = query.filter(
332-
account__provider__in=[
351+
remote_repository_relations__account__provider__in=[
333352
service.adapter.provider_id for service in registry
334353
],
335-
)
354+
).distinct()
336355

337356
# optimizes for the RemoteOrganizationSerializer
338-
query = query.select_related('organization')
357+
query = query.select_related('organization').order_by(
358+
'organization__name', 'full_name'
359+
)
339360

340361
return query
341362

readthedocs/builds/models.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@
6666
)
6767
from readthedocs.builds.version_slug import VersionSlugField
6868
from readthedocs.config import LATEST_CONFIGURATION_VERSION
69-
from readthedocs.core.utils import broadcast
7069
from readthedocs.projects.constants import (
7170
BITBUCKET_COMMIT_URL,
7271
BITBUCKET_URL,

readthedocs/core/signals.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def decide_if_cors(sender, request, **kwargs): # pylint: disable=unused-argumen
8686

8787

8888
@receiver(pre_delete, sender=settings.AUTH_USER_MODEL)
89-
def delete_projects_and_organizations(sender, instance, *args, **kwargs):
89+
def delete_projects(sender, instance, *args, **kwargs):
9090
# Here we count the owner list from the projects that the user own
9191
# Then exclude the projects where there are more than one owner
9292
# Add annotate before filter
@@ -98,16 +98,7 @@ def delete_projects_and_organizations(sender, instance, *args, **kwargs):
9898
).exclude(num_users__gt=1)
9999
)
100100

101-
# Here we count the users list from the organization that the user belong
102-
# Then exclude the organizations where there are more than one user
103-
oauth_organizations = (
104-
RemoteOrganization.objects.annotate(num_users=Count('users')
105-
).filter(users=instance.id
106-
).exclude(num_users__gt=1)
107-
)
108-
109101
projects.delete()
110-
oauth_organizations.delete()
111102

112103

113104
signals.check_request_enabled.connect(decide_if_cors)
Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,50 @@
11
# -*- coding: utf-8 -*-
22
import django_dynamic_fixture
33
import pytest
4+
45
from django.contrib.auth.models import User
56

6-
from readthedocs.oauth.models import RemoteOrganization
77
from readthedocs.projects.models import Project
88

99

1010
@pytest.mark.django_db
1111
class TestProjectOrganizationSignal:
1212

13-
@pytest.mark.parametrize('model_class', [Project, RemoteOrganization])
14-
def test_project_organization_get_deleted_upon_user_delete(self, model_class):
15-
"""If the user has Project or RemoteOrganization where he is the only
16-
user, upon deleting his account, the Project or RemoteOrganization
13+
def test_project_get_deleted_upon_user_delete(self):
14+
"""If the user has Project where he is the only
15+
user, upon deleting his account, the Project
1716
should also get deleted."""
1817

19-
obj = django_dynamic_fixture.get(model_class)
18+
project = django_dynamic_fixture.get(Project)
2019
user1 = django_dynamic_fixture.get(User)
21-
obj.users.add(user1)
20+
project.users.add(user1)
2221

23-
obj.refresh_from_db()
24-
assert obj.users.all().count() == 1
22+
project.refresh_from_db()
23+
assert project.users.all().count() == 1
2524

2625
# Delete the user
2726
user1.delete()
2827
# The object should not exist
29-
obj = model_class.objects.all().filter(id=obj.id)
30-
assert not obj.exists()
28+
project = Project.objects.all().filter(id=project.id)
29+
assert not project.exists()
3130

32-
@pytest.mark.parametrize('model_class', [Project, RemoteOrganization])
33-
def test_multiple_users_project_organization_not_delete(self, model_class):
34-
"""Check Project or RemoteOrganization which have multiple users do not
31+
def test_multiple_users_project_not_delete(self):
32+
"""Check Project which have multiple users do not
3533
get deleted when any of the user delete his account."""
3634

37-
obj = django_dynamic_fixture.get(model_class)
35+
project = django_dynamic_fixture.get(Project)
3836
user1 = django_dynamic_fixture.get(User)
3937
user2 = django_dynamic_fixture.get(User)
40-
obj.users.add(user1, user2)
38+
project.users.add(user1, user2)
4139

42-
obj.refresh_from_db()
43-
assert obj.users.all().count() > 1
40+
project.refresh_from_db()
41+
assert project.users.all().count() > 1
4442
# Delete 1 user of the project
4543
user1.delete()
4644

4745
# The project should still exist and it should have 1 user
48-
obj.refresh_from_db()
49-
assert obj.id
50-
obj_users = obj.users.all()
46+
project.refresh_from_db()
47+
assert project.id
48+
obj_users = project.users.all()
5149
assert len(obj_users) == 1
5250
assert user2 in obj_users

readthedocs/oauth/admin.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,38 @@
44

55
from django.contrib import admin
66

7-
from .models import RemoteOrganization, RemoteRepository
7+
from .models import (
8+
RemoteOrganization,
9+
RemoteOrganizationRelation,
10+
RemoteRepository,
11+
RemoteRepositoryRelation,
12+
)
813

914

1015
class RemoteRepositoryAdmin(admin.ModelAdmin):
1116

1217
"""Admin configuration for the RemoteRepository model."""
1318

14-
raw_id_fields = ('users',)
19+
raw_id_fields = ('project', 'organization',)
1520

1621

17-
class RemoteOrganizationAdmin(admin.ModelAdmin):
22+
class RemoteRepositoryRelationAdmin(admin.ModelAdmin):
1823

19-
"""Admin configuration for the RemoteOrganization model."""
24+
"""Admin configuration for the RemoteRepositoryRelation model."""
2025

21-
raw_id_fields = ('users',)
26+
raw_id_fields = ('account', 'remote_repository', 'user',)
27+
list_select_related = ('remote_repository', 'user',)
28+
29+
30+
class RemoteOrganizationRelationAdmin(admin.ModelAdmin):
31+
32+
"""Admin configuration for the RemoteOrganizationRelation model."""
33+
34+
raw_id_fields = ('account', 'remote_organization', 'user',)
35+
list_select_related = ('remote_organization', 'user',)
2236

2337

2438
admin.site.register(RemoteRepository, RemoteRepositoryAdmin)
25-
admin.site.register(RemoteOrganization, RemoteOrganizationAdmin)
39+
admin.site.register(RemoteRepositoryRelation, RemoteRepositoryRelationAdmin)
40+
admin.site.register(RemoteOrganization)
41+
admin.site.register(RemoteOrganizationRelation, RemoteOrganizationRelationAdmin)

readthedocs/oauth/constants.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
GITHUB = 'github'
2+
GITLAB = 'gitlab'
3+
BITBUCKET = 'bitbucket'
4+
5+
VCS_PROVIDER_CHOICES = (
6+
(GITHUB, 'GitHub'),
7+
(GITLAB, 'GitLab'),
8+
(BITBUCKET, 'Bitbucket'),
9+
)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import json
2+
3+
from django.core.management.base import BaseCommand
4+
5+
from readthedocs.oauth.models import RemoteRepository
6+
7+
8+
class Command(BaseCommand):
9+
help = "Load Project and RemoteRepository Relationship from JSON file"
10+
11+
def add_arguments(self, parser):
12+
# File path of the json file containing relationship data
13+
parser.add_argument(
14+
'--file',
15+
required=True,
16+
nargs=1,
17+
type=str,
18+
help='File path of the json file containing relationship data.',
19+
)
20+
21+
def handle(self, *args, **options):
22+
file = options.get('file')[0]
23+
24+
try:
25+
# Load data from the json file
26+
with open(file, 'r') as f:
27+
data = json.load(f)
28+
except Exception as e:
29+
self.stdout.write(
30+
self.style.ERROR(
31+
f'Exception occurred while trying to load the file "{file}". '
32+
f'Exception: {e}.'
33+
)
34+
)
35+
return
36+
37+
for item in data:
38+
try:
39+
update_count = RemoteRepository.objects.filter(
40+
remote_id=item['remote_id']
41+
).update(project_id=item['project_id'])
42+
43+
if update_count < 1:
44+
self.stdout.write(
45+
self.style.ERROR(
46+
f"Could not update {item['slug']}'s "
47+
f"relationship with {item['html_url']}, "
48+
f"remote_id {item['remote_id']}, "
49+
f"username: {item['username']}."
50+
)
51+
)
52+
53+
except Exception as e:
54+
self.stdout.write(
55+
self.style.ERROR(
56+
f"Exception occurred while trying to update {item['slug']}'s "
57+
f"relationship with {item['html_url']}, "
58+
f"username: {item['username']}, Exception: {e}."
59+
)
60+
)

readthedocs/oauth/management/commands/reconnect_remoterepositories.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def _connect_repositories(self, organization, no_dry_run, only_owners):
5555
Q(ssh_url__in=Subquery(organization.projects.values('repo'))) |
5656
Q(clone_url__in=Subquery(organization.projects.values('repo')))
5757
)
58-
for remote in RemoteRepository.objects.filter(remote_query).order_by('pub_date'):
58+
for remote in RemoteRepository.objects.filter(remote_query).order_by('created'):
5959
admin = json.loads(remote.json).get('permissions', {}).get('admin')
6060

6161
if only_owners and remote.users.first() not in organization.owners.all():

0 commit comments

Comments
 (0)