diff --git a/docs/api/v3.rst b/docs/api/v3.rst index 502abc5fa55..a7b28885c2f 100644 --- a/docs/api/v3.rst +++ b/docs/api/v3.rst @@ -1713,12 +1713,12 @@ Remote Repository listing .. code-tab:: bash - $ curl -H "Authorization: Token " https://readthedocs.org/api/v3/remote/repositories/?expand=project,organization + $ curl -H "Authorization: Token " https://readthedocs.org/api/v3/remote/repositories/?expand=projects,remote_organization .. code-tab:: python import requests - URL = 'https://readthedocs.org/api/v3/remote/repositories/?expand=project,organization' + URL = 'https://readthedocs.org/api/v3/remote/repositories/?expand=projects,remote_organization' TOKEN = '' HEADERS = {'Authorization': f'token {TOKEN}'} response = requests.get(URL, headers=HEADERS) @@ -1730,11 +1730,11 @@ Remote Repository listing { "count": 20, - "next": "api/v3/remote/repositories/?expand=project,organization&limit=10&offset=10", + "next": "api/v3/remote/repositories/?expand=projects,remote_organization&limit=10&offset=10", "previous": null, "results": [ { - "organization": { + "remote_organization": { "avatar_url": "https://avatars.githubusercontent.com/u/12345?v=4", "created": "2019-04-29T10:00:00Z", "modified": "2019-04-29T12:00:00Z", @@ -1744,7 +1744,7 @@ Remote Repository listing "url": "https://github.com/organization", "vcs_provider": "github" }, - "project": { + "project": [{ "id": 12345, "name": "project", "slug": "project", @@ -1787,7 +1787,7 @@ Remote Repository listing "redirects": "/api/v3/projects/project/redirects/", "translations": "/api/v3/projects/project/translations/" } - } + }], "avatar_url": "https://avatars3.githubusercontent.com/u/test-organization?v=4", "clone_url": "https://github.com/organization/project.git", "created": "2019-04-29T10:00:00Z", @@ -1814,7 +1814,7 @@ Remote Repository listing :query string vcs_provider: return remote repositories for specific vcs provider (``github``, ``gitlab`` or ``bitbucket``) :query string organization: return remote repositories for specific remote organization (using remote organization ``slug``) :query string expand: allows to add/expand some extra fields in the response. - Allowed values are ``project`` and ``organization``. + Allowed values are ``projects`` and ``remote_organization``. Multiple fields can be passed separated by commas. :requestheader Authorization: token to authenticate. diff --git a/readthedocs/api/v3/serializers.py b/readthedocs/api/v3/serializers.py index e10a67575a0..b854839f2c9 100644 --- a/readthedocs/api/v3/serializers.py +++ b/readthedocs/api/v3/serializers.py @@ -950,11 +950,11 @@ class Meta: ] read_only_fields = fields expandable_fields = { - 'organization': ( + 'remote_organization': ( RemoteOrganizationSerializer, {'source': 'organization'} ), - 'project': ( - ProjectSerializer, {'source': 'project'} + 'projects': ( + ProjectSerializer, {'source': 'projects', 'many': True} ) } diff --git a/readthedocs/api/v3/tests/responses/remoterepositories-list.json b/readthedocs/api/v3/tests/responses/remoterepositories-list.json index 050f1238009..3112588c3a4 100644 --- a/readthedocs/api/v3/tests/responses/remoterepositories-list.json +++ b/readthedocs/api/v3/tests/responses/remoterepositories-list.json @@ -14,19 +14,9 @@ "html_url": "https://github.com/rtd/project", "modified": "2019-04-29T12:00:00Z", "name": "project", - "organization": { - "avatar_url": "https://avatars.githubusercontent.com/u/366329?v=4", - "created": "2019-04-29T10:00:00Z", - "modified": "2019-04-29T12:00:00Z", - "name": "Read the Docs", - "pk": 1, - "slug": "readthedocs", - "url": "https://github.com/readthedocs", - "vcs_provider": "github" - }, "pk": 1, "private": false, - "project": { + "projects": [{ "_links": { "_self": "https://readthedocs.org/api/v3/projects/project/", "builds": "https://readthedocs.org/api/v3/projects/project/builds/", @@ -58,6 +48,16 @@ "versions": "https://readthedocs.org/projects/project/versions/" }, "users": [{ "username": "testuser" }] + }], + "remote_organization": { + "avatar_url": "https://avatars.githubusercontent.com/u/366329?v=4", + "created": "2019-04-29T10:00:00Z", + "modified": "2019-04-29T12:00:00Z", + "name": "Read the Docs", + "pk": 1, + "slug": "readthedocs", + "url": "https://github.com/readthedocs", + "vcs_provider": "github" }, "ssh_url": "git@github.com:rtd/project.git", "vcs": "git", diff --git a/readthedocs/api/v3/tests/test_remoterepositories.py b/readthedocs/api/v3/tests/test_remoterepositories.py index 58e2745c01f..0b1c164d533 100644 --- a/readthedocs/api/v3/tests/test_remoterepositories.py +++ b/readthedocs/api/v3/tests/test_remoterepositories.py @@ -33,7 +33,6 @@ def setUp(self): self.remote_repository = fixture.get( RemoteRepository, - project=self.project, organization=self.remote_organization, created=self.created, modified=self.modified, @@ -49,6 +48,8 @@ def setUp(self): default_branch="master", private=False ) + self.remote_repository.projects.add(self.project) + social_account = fixture.get(SocialAccount, user=self.me, provider=GITHUB) fixture.get( RemoteRepositoryRelation, @@ -70,8 +71,8 @@ def test_remote_repository_list(self): reverse('remoterepositories-list'), { 'expand': ( - 'project,' - 'organization' + 'projects,' + 'remote_organization' ) } ) @@ -88,8 +89,8 @@ def test_remote_repository_list_name_filter(self): reverse('remoterepositories-list'), { 'expand': ( - 'project,' - 'organization' + 'projects,' + 'remote_organization' ), 'name': 'proj' } diff --git a/readthedocs/api/v3/views.py b/readthedocs/api/v3/views.py index cb7cd5fd96d..5d81999e7a0 100644 --- a/readthedocs/api/v3/views.py +++ b/readthedocs/api/v3/views.py @@ -451,8 +451,8 @@ class RemoteRepositoryViewSet( queryset = RemoteRepository.objects.all() permission_classes = (IsAuthenticated,) permit_list_expands = [ - 'organization', - 'project' + 'remote_organization', + 'projects' ] def get_queryset(self): @@ -466,13 +466,11 @@ def get_queryset(self): ) ) - if is_expanded(self.request, 'organization'): + if is_expanded(self.request, 'remote_organization'): queryset = queryset.select_related('organization') - if is_expanded(self.request, 'project'): - queryset = queryset.select_related('project').prefetch_related( - 'project__users', - ) + if is_expanded(self.request, 'projects'): + queryset = queryset.prefetch_related('projects__users') return queryset.order_by('organization__name', 'full_name').distinct() diff --git a/readthedocs/builds/tasks.py b/readthedocs/builds/tasks.py index 7d03e65c330..104b081a746 100644 --- a/readthedocs/builds/tasks.py +++ b/readthedocs/builds/tasks.py @@ -371,7 +371,7 @@ def send_build_status(build_pk, commit, status, link_to_build=False): service_class = build.project.git_service_class() users = build.project.users.all() - try: + if build.project.remote_repository: remote_repository = build.project.remote_repository remote_repository_relations = ( remote_repository.remote_repository_relations.filter( @@ -406,8 +406,7 @@ def send_build_status(build_pk, commit, status, link_to_build=False): relation.user.username, ) return True - - except RemoteRepository.DoesNotExist: + else: log.warning( 'Project does not have a RemoteRepository. project=%s', build.project.slug, diff --git a/readthedocs/oauth/admin.py b/readthedocs/oauth/admin.py index d6e43f34740..035dd941ac4 100644 --- a/readthedocs/oauth/admin.py +++ b/readthedocs/oauth/admin.py @@ -16,7 +16,7 @@ class RemoteRepositoryAdmin(admin.ModelAdmin): """Admin configuration for the RemoteRepository model.""" - raw_id_fields = ('project', 'organization',) + raw_id_fields = ('organization',) class RemoteRepositoryRelationAdmin(admin.ModelAdmin): diff --git a/readthedocs/oauth/migrations/0014_remove_remoterepository_project.py b/readthedocs/oauth/migrations/0014_remove_remoterepository_project.py new file mode 100644 index 00000000000..7d987debb96 --- /dev/null +++ b/readthedocs/oauth/migrations/0014_remove_remoterepository_project.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2.22 on 2021-06-14 15:42 + +from django.db import migrations + + +def migrate_data(apps, schema_editor): + RemoteRepository = apps.get_model('oauth', 'RemoteRepository') + queryset = RemoteRepository.objects.filter(project__isnull=False).select_related('project') + for rr in queryset.iterator(): + rr.project.remote_repository = rr + rr.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('oauth', '0013_create_new_table_for_remote_repository_normalization'), + ('projects', '0076_project_remote_repository'), + ] + + operations = [ + migrations.RunPython(migrate_data), + migrations.RemoveField( + model_name='remoterepository', + name='project', + ), + ] diff --git a/readthedocs/oauth/models.py b/readthedocs/oauth/models.py index 5cfe2266ee5..0f14c2ba194 100644 --- a/readthedocs/oauth/models.py +++ b/readthedocs/oauth/models.py @@ -124,13 +124,6 @@ class RemoteRepository(TimeStampedModel): blank=True, on_delete=models.CASCADE, ) - project = models.OneToOneField( - Project, - on_delete=models.SET_NULL, - related_name='remote_repository', - null=True, - blank=True, - ) name = models.CharField(_('Name'), max_length=255) full_name = models.CharField( _('Full Name'), diff --git a/readthedocs/oauth/services/gitlab.py b/readthedocs/oauth/services/gitlab.py index 6213c876968..d2322cf9fef 100644 --- a/readthedocs/oauth/services/gitlab.py +++ b/readthedocs/oauth/services/gitlab.py @@ -54,9 +54,9 @@ class GitLabService(Service): def _get_repo_id(self, project): # The ID or URL-encoded path of the project # https://docs.gitlab.com/ce/api/README.html#namespaced-path-encoding - try: + if project.remote_repository: repo_id = project.remote_repository.remote_id - except Project.remote_repository.RelatedObjectDoesNotExist: + else: # Handle "Manual Import" when there is no RemoteRepository # associated with the project. It only works with gitlab.com at the # moment (doesn't support custom gitlab installations) diff --git a/readthedocs/projects/admin.py b/readthedocs/projects/admin.py index 64b5ceaeefe..f6363e3da30 100644 --- a/readthedocs/projects/admin.py +++ b/readthedocs/projects/admin.py @@ -165,7 +165,7 @@ class ProjectAdmin(admin.ModelAdmin): DomainInline, ] readonly_fields = ('pub_date', 'feature_flags',) - raw_id_fields = ('users', 'main_language_project') + raw_id_fields = ('users', 'main_language_project', 'remote_repository') actions = [ 'send_owner_email', 'ban_owner', diff --git a/readthedocs/projects/forms.py b/readthedocs/projects/forms.py index 0c2f5c71771..0e6bd186ad7 100644 --- a/readthedocs/projects/forms.py +++ b/readthedocs/projects/forms.py @@ -107,7 +107,7 @@ def save(self, commit=True): remote_repo = self.cleaned_data.get('remote_repository', None) if remote_repo: if commit: - remote_repo.project = self.instance + remote_repo.projects.add(self.instance) remote_repo.save() else: instance.remote_repository = remote_repo diff --git a/readthedocs/projects/migrations/0076_project_remote_repository.py b/readthedocs/projects/migrations/0076_project_remote_repository.py new file mode 100644 index 00000000000..e7c8877a438 --- /dev/null +++ b/readthedocs/projects/migrations/0076_project_remote_repository.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.22 on 2021-06-14 15:42 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('oauth', '0013_create_new_table_for_remote_repository_normalization'), + ('projects', '0075_change_mkdocs_name'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='remote_repository', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projects', to='oauth.RemoteRepository'), + ), + ] diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index e19fb84948b..c5d7f6000ec 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -423,6 +423,14 @@ class Project(models.Model): objects = ProjectQuerySet.as_manager() all_objects = models.Manager() + remote_repository = models.ForeignKey( + 'oauth.RemoteRepository', + on_delete=models.SET_NULL, + related_name='projects', + null=True, + blank=True, + ) + # Property used for storing the latest build for a project when prefetching LATEST_BUILD_CACHE = '_latest_build' diff --git a/readthedocs/rtd_tests/tests/test_celery.py b/readthedocs/rtd_tests/tests/test_celery.py index 701178ebe71..4f37d241ad3 100644 --- a/readthedocs/rtd_tests/tests/test_celery.py +++ b/readthedocs/rtd_tests/tests/test_celery.py @@ -352,7 +352,8 @@ def test_send_build_status_with_remote_repo_github(self, send_build_status): self.project.save() social_account = get(SocialAccount, user=self.eric, provider='gitlab') - remote_repo = get(RemoteRepository, project=self.project) + remote_repo = get(RemoteRepository) + remote_repo.projects.add(self.project) get( RemoteRepositoryRelation, remote_repository=remote_repo, @@ -417,7 +418,8 @@ def test_send_build_status_with_remote_repo_gitlab(self, send_build_status): self.project.save() social_account = get(SocialAccount, user=self.eric, provider='gitlab') - remote_repo = get(RemoteRepository, project=self.project) + remote_repo = get(RemoteRepository) + remote_repo.projects.add(self.project) get( RemoteRepositoryRelation, remote_repository=remote_repo, diff --git a/readthedocs/rtd_tests/tests/test_oauth_sync.py b/readthedocs/rtd_tests/tests/test_oauth_sync.py index 70db64b07b9..66ee30a78d4 100644 --- a/readthedocs/rtd_tests/tests/test_oauth_sync.py +++ b/readthedocs/rtd_tests/tests/test_oauth_sync.py @@ -109,11 +109,11 @@ def test_sync_delete_stale(self, mock_request): project = fixture.get(Project) repo_3 = fixture.get( RemoteRepository, - project=project, full_name='organization/project-linked-repository', remote_id='54321', vcs_provider=GITHUB ) + repo_3.projects.add(project) fixture.get( RemoteRepositoryRelation, remote_repository=repo_3,