diff --git a/readthedocs/projects/exceptions.py b/readthedocs/projects/exceptions.py index 6acec396812..ba4196320a1 100644 --- a/readthedocs/projects/exceptions.py +++ b/readthedocs/projects/exceptions.py @@ -43,6 +43,10 @@ class RepositoryError(BuildEnvironmentError): 'One or more submodule URLs are not valid.' ) + DUPLICATED_RESERVED_VERSIONS = _( + 'You can not have two versions with the name latest or stable.' + ) + def get_default_message(self): if settings.ALLOW_PRIVATE_REPOS: return self.PRIVATE_ALLOWED diff --git a/readthedocs/projects/tasks.py b/readthedocs/projects/tasks.py index 8e063b255f7..9014fda9f2a 100644 --- a/readthedocs/projects/tasks.py +++ b/readthedocs/projects/tasks.py @@ -5,7 +5,8 @@ rebuilding documentation. """ -from __future__ import absolute_import +from __future__ import ( + absolute_import, division, print_function, unicode_literals) import datetime import hashlib @@ -14,7 +15,7 @@ import os import shutil import socket -from collections import defaultdict +from collections import Counter, defaultdict import requests from builtins import str @@ -31,12 +32,10 @@ from .exceptions import RepositoryError from .models import ImportedFile, Project, Domain from .signals import before_vcs, after_vcs, before_build, after_build, files_changed -from readthedocs.builds.constants import (LATEST, - BUILD_STATE_CLONING, - BUILD_STATE_INSTALLING, - BUILD_STATE_BUILDING, - BUILD_STATE_FINISHED) -from readthedocs.builds.models import Build, Version, APIVersion +from readthedocs.builds.constants import ( + BUILD_STATE_BUILDING, BUILD_STATE_CLONING, BUILD_STATE_FINISHED, + BUILD_STATE_INSTALLING, LATEST, LATEST_VERBOSE_NAME, STABLE_VERBOSE_NAME) +from readthedocs.builds.models import APIVersion, Build, Version from readthedocs.builds.signals import build_complete from readthedocs.builds.syncers import Syncer from readthedocs.core.resolver import resolve_path @@ -141,6 +140,8 @@ def sync_repo(self): } for v in version_repo.branches ] + self.validate_duplicate_reserved_versions(version_post_data) + try: # Hit the API ``sync_versions`` which may trigger a new build # for the stable version @@ -150,6 +151,27 @@ def sync_repo(self): except Exception: log.exception('Unknown Sync Versions Exception') + def validate_duplicate_reserved_versions(self, data): + """ + Check if there are duplicated names of reserved versions. + + The user can't have a branch and a tag with the same name of + ``latest`` or ``stable``. Raise a RepositoryError exception + if there is a duplicated name. + + :param data: Dict containing the versions from tags and branches + """ + version_names = [ + version['verbose_name'] + for version in data.get('tags', []) + data.get('branches', []) + ] + counter = Counter(version_names) + for reserved_name in [STABLE_VERBOSE_NAME, LATEST_VERBOSE_NAME]: + if counter[reserved_name] > 1: + raise RepositoryError( + RepositoryError.DUPLICATED_RESERVED_VERSIONS + ) + # TODO this is duplicated in the classes below, and this should be # refactored out anyways, as calling from the method removes the original # caller from logging. diff --git a/readthedocs/restapi/utils.py b/readthedocs/restapi/utils.py index 00e9ec15937..9e7f73bfa43 100644 --- a/readthedocs/restapi/utils.py +++ b/readthedocs/restapi/utils.py @@ -9,7 +9,9 @@ from rest_framework.pagination import PageNumberPagination -from readthedocs.builds.constants import NON_REPOSITORY_VERSIONS +from readthedocs.builds.constants import (LATEST, LATEST_VERBOSE_NAME, + NON_REPOSITORY_VERSIONS, STABLE, + STABLE_VERBOSE_NAME) from readthedocs.builds.models import Version from readthedocs.search.indexes import PageIndex, ProjectIndex, SectionIndex @@ -18,18 +20,41 @@ def sync_versions(project, versions, type): # pylint: disable=redefined-builtin """Update the database with the current versions from the repository.""" - old_versions = {} - old_version_values = project.versions.filter(type=type).values( - 'identifier', 'verbose_name') - for version in old_version_values: - old_versions[version['verbose_name']] = version['identifier'] + old_version_values = project.versions.filter(type=type).values_list( + 'verbose_name', 'identifier' + ) + old_versions = dict(old_version_values) - added = set() # Add new versions + added = set() + has_user_stable = False + has_user_latest = False for version in versions: version_id = version['identifier'] version_name = version['verbose_name'] - if version_name in old_versions: + if version_name == STABLE_VERBOSE_NAME: + has_user_stable = True + created_version, created = set_or_create_version( + project=project, + slug=STABLE, + version_id=version_id, + verbose_name=version_name, + type_=type + ) + if created: + added.add(created_version.slug) + elif version_name == LATEST_VERBOSE_NAME: + has_user_latest = True + created_version, created = set_or_create_version( + project=project, + slug=LATEST, + version_id=version_id, + verbose_name=version_name, + type_=type + ) + if created: + added.add(created_version.slug) + elif version_name in old_versions: if version_id == old_versions[version_name]: # Version is correct continue @@ -44,23 +69,68 @@ def sync_versions(project, versions, type): # pylint: disable=redefined-builtin log.info( '(Sync Versions) Updated Version: [%s=%s] ', - version['verbose_name'], - version['identifier'], + version_name, + version_id, ) else: # New Version created_version = Version.objects.create( project=project, type=type, - identifier=version['identifier'], - verbose_name=version['verbose_name'], + identifier=version_id, + verbose_name=version_name, ) added.add(created_version.slug) + if not has_user_stable: + stable_version = ( + project.versions + .filter(slug=STABLE, type=type) + .first() + ) + if stable_version: + # Put back the RTD's stable version + stable_version.machine = True + stable_version.save() + if not has_user_latest: + latest_version = ( + project.versions + .filter(slug=LATEST, type=type) + .first() + ) + if latest_version: + # Put back the RTD's latest version + latest_version.machine = True + latest_version.identifier = project.get_default_branch() + latest_version.verbose_name = LATEST_VERBOSE_NAME + latest_version.save() if added: log.info('(Sync Versions) Added Versions: [%s] ', ' '.join(added)) return added +def set_or_create_version(project, slug, version_id, verbose_name, type_): + """Search or create a version and set its machine atribute to false.""" + version = ( + project.versions + .filter(slug=slug) + .first() + ) + if version: + version.identifier = version_id + version.machine = False + version.type = type_ + version.save() + else: + created_version = Version.objects.create( + project=project, + type=type_, + identifier=version_id, + verbose_name=verbose_name, + ) + return created_version, True + return version, False + + def delete_versions(project, version_data): """Delete all versions not in the current repo.""" current_versions = [] diff --git a/readthedocs/rtd_tests/tests/test_celery.py b/readthedocs/rtd_tests/tests/test_celery.py index 9fa36b6e5ba..1a8e9fc15d6 100644 --- a/readthedocs/rtd_tests/tests/test_celery.py +++ b/readthedocs/rtd_tests/tests/test_celery.py @@ -1,3 +1,5 @@ +from __future__ import division, print_function, unicode_literals + import os import json import shutil @@ -9,14 +11,19 @@ from mock import patch, MagicMock from readthedocs.builds.constants import BUILD_STATE_INSTALLING, BUILD_STATE_FINISHED, LATEST +from readthedocs.projects.exceptions import RepositoryError from readthedocs.builds.models import Build from readthedocs.projects.models import Project from readthedocs.projects import tasks +from readthedocs.rtd_tests.utils import ( + create_git_branch, create_git_tag, delete_git_branch, delete_git_tag) from readthedocs.rtd_tests.utils import make_test_git from readthedocs.rtd_tests.base import RTDTestCase from readthedocs.rtd_tests.mocks.mock_api import mock_api +from readthedocs.doc_builder.environments import BuildEnvironment + class TestCeleryBuilding(RTDTestCase): @@ -124,6 +131,58 @@ def test_sync_repository(self): ) self.assertTrue(result.successful()) + @patch('readthedocs.projects.tasks.api_v2') + def test_check_duplicate_reserved_version_latest(self, api_v2): + create_git_branch(self.repo, 'latest') + create_git_tag(self.repo, 'latest') + + version = self.project.versions.get(slug=LATEST) + sync_repository = tasks.UpdateDocsTaskStep() + sync_repository.version = version + sync_repository.project = self.project + with self.assertRaises(RepositoryError) as e: + sync_repository.sync_repo() + self.assertEqual( + str(e.exception), + RepositoryError.DUPLICATED_RESERVED_VERSIONS + ) + + delete_git_branch(self.repo, 'latest') + sync_repository.sync_repo() + api_v2.project().sync_versions.post.assert_called() + + @patch('readthedocs.projects.tasks.api_v2') + def test_check_duplicate_reserved_version_stable(self, api_v2): + create_git_branch(self.repo, 'stable') + create_git_tag(self.repo, 'stable') + + version = self.project.versions.get(slug=LATEST) + sync_repository = tasks.UpdateDocsTaskStep() + sync_repository.version = version + sync_repository.project = self.project + with self.assertRaises(RepositoryError) as e: + sync_repository.sync_repo() + self.assertEqual( + str(e.exception), + RepositoryError.DUPLICATED_RESERVED_VERSIONS + ) + + # TODO: Check that we can build properly after + # deleting the tag. + + @patch('readthedocs.projects.tasks.api_v2') + def test_check_duplicate_no_reserved_version(self, api_v2): + create_git_branch(self.repo, 'no-reserved') + create_git_tag(self.repo, 'no-reserved') + + version = self.project.versions.get(slug=LATEST) + sync_repository = tasks.UpdateDocsTaskStep() + sync_repository.version = version + sync_repository.project = self.project + sync_repository.sync_repo() + + api_v2.project().sync_versions.post.assert_called() + def test_public_task_exception(self): """ Test when a PublicTask rises an Exception. diff --git a/readthedocs/rtd_tests/tests/test_sync_versions.py b/readthedocs/rtd_tests/tests/test_sync_versions.py index 945a765bb6b..7612ec49425 100644 --- a/readthedocs/rtd_tests/tests/test_sync_versions.py +++ b/readthedocs/rtd_tests/tests/test_sync_versions.py @@ -6,6 +6,8 @@ import json from django.test import TestCase +from django.core.urlresolvers import reverse +import pytest from readthedocs.builds.constants import BRANCH, STABLE, TAG from readthedocs.builds.models import Version @@ -152,6 +154,503 @@ def test_new_tag_update_inactive(self): version_8 = Version.objects.get(slug='0.8.3') self.assertFalse(version_8.active) + def test_delete_version(self): + Version.objects.create( + project=self.pip, + identifier='0.8.3', + verbose_name='0.8.3', + active=False, + ) + + version_post_data = { + 'branches': [ + { + 'identifier': 'origin/master', + 'verbose_name': 'master', + }, + ], + } + + self.assertTrue( + Version.objects.filter(slug='0.8.3').exists() + ) + + self.client.post( + reverse('project-sync-versions', args=[self.pip.pk]), + data=json.dumps(version_post_data), + content_type='application/json', + ) + + # There isn't a v0.8.3 + self.assertFalse( + Version.objects.filter(slug='0.8.3').exists() + ) + + def test_machine_attr_when_user_define_stable_tag_and_delete_it(self): + """ + The user creates a tag named ``stable`` on an existing repo, + when syncing the versions, the RTD's ``stable`` is lost + (set to machine=False) and doesn't update automatically anymore, + when the tag is deleted on the user repository, the RTD's ``stable`` + is back (set to machine=True). + """ + version8 = Version.objects.create( + project=self.pip, + identifier='0.8.3', + verbose_name='0.8.3', + type=TAG, + active=False, + machine=False, + ) + self.pip.update_stable_version() + current_stable = self.pip.get_stable_version() + + # 0.8.3 is the current stable + self.assertEqual( + version8.identifier, + current_stable.identifier + ) + self.assertTrue(current_stable.machine) + + version_post_data = { + 'branches': [ + { + 'identifier': 'origin/master', + 'verbose_name': 'master', + }, + ], + 'tags': [ + # User new stable + { + 'identifier': '1abc2def3', + 'verbose_name': 'stable', + }, + { + 'identifier': '0.8.3', + 'verbose_name': '0.8.3', + }, + ], + } + + resp = self.client.post( + reverse('project-sync-versions', args=[self.pip.pk]), + data=json.dumps(version_post_data), + content_type='application/json', + ) + self.assertEqual(resp.status_code, 200) + + current_stable = self.pip.get_stable_version() + self.assertEqual( + '1abc2def3', + current_stable.identifier + ) + + # Deleting the tag should return the RTD's stable + version_post_data = { + 'branches': [ + { + 'identifier': 'origin/master', + 'verbose_name': 'master', + }, + ], + 'tags': [ + { + 'identifier': '0.8.3', + 'verbose_name': '0.8.3', + }, + ], + } + + resp = self.client.post( + reverse('project-sync-versions', args=[self.pip.pk]), + data=json.dumps(version_post_data), + content_type='application/json', + ) + self.assertEqual(resp.status_code, 200) + + # The version 8 should be the new stable. + # The stable isn't stuck with the previous commit + current_stable = self.pip.get_stable_version() + self.assertEqual( + '0.8.3', + current_stable.identifier + ) + self.assertTrue(current_stable.machine) + + def test_machine_attr_when_user_define_stable_tag_and_delete_it_new_project(self): + """ + The user imports a new project with a tag named ``stable``, + when syncing the versions, the RTD's ``stable`` is lost + (set to machine=False) and doesn't update automatically anymore, + when the tag is deleted on the user repository, the RTD's ``stable`` + is back (set to machine=True). + """ + # There isn't a stable version yet + self.pip.versions.exclude(slug='master').delete() + current_stable = self.pip.get_stable_version() + self.assertIsNone(current_stable) + + version_post_data = { + 'branches': [ + { + 'identifier': 'origin/master', + 'verbose_name': 'master', + }, + ], + 'tags': [ + # User stable + { + 'identifier': '1abc2def3', + 'verbose_name': 'stable', + }, + { + 'identifier': '0.8.3', + 'verbose_name': '0.8.3', + }, + ], + } + + resp = self.client.post( + reverse('project-sync-versions', args=[self.pip.pk]), + data=json.dumps(version_post_data), + content_type='application/json', + ) + self.assertEqual(resp.status_code, 200) + + current_stable = self.pip.get_stable_version() + self.assertEqual( + '1abc2def3', + current_stable.identifier + ) + + # User activates the stable version + current_stable.active = True + current_stable.save() + + # Deleting the tag should return the RTD's stable + version_post_data = { + 'branches': [ + { + 'identifier': 'origin/master', + 'verbose_name': 'master', + }, + ], + 'tags': [ + { + 'identifier': '0.8.3', + 'verbose_name': '0.8.3', + }, + ], + } + + resp = self.client.post( + reverse('project-sync-versions', args=[self.pip.pk]), + data=json.dumps(version_post_data), + content_type='application/json', + ) + self.assertEqual(resp.status_code, 200) + + # The version 8 should be the new stable. + # The stable isn't stuck with the previous commit + current_stable = self.pip.get_stable_version() + self.assertEqual( + '0.8.3', + current_stable.identifier + ) + self.assertTrue(current_stable.machine) + + def test_machine_attr_when_user_define_stable_branch_and_delete_it(self): + """ + The user creates a branch named ``stable`` on an existing repo, + when syncing the versions, the RTD's ``stable`` is lost + (set to machine=False) and doesn't update automatically anymore, + when the branch is deleted on the user repository, the RTD's ``stable`` + is back (set to machine=True). + """ + # Project with just branches + self.pip.versions.filter(type=TAG).delete() + Version.objects.create( + project=self.pip, + identifier='0.8.3', + verbose_name='0.8.3', + type=BRANCH, + active=False, + machine=False, + ) + self.pip.update_stable_version() + current_stable = self.pip.get_stable_version() + + # 0.8.3 is the current stable + self.assertEqual( + '0.8.3', + current_stable.identifier + ) + self.assertTrue(current_stable.machine) + + version_post_data = { + 'branches': [ + { + 'identifier': 'origin/master', + 'verbose_name': 'master', + }, + # User new stable + { + 'identifier': 'origin/stable', + 'verbose_name': 'stable', + }, + { + 'identifier': 'origin/0.8.3', + 'verbose_name': '0.8.3', + }, + ], + } + + resp = self.client.post( + reverse('project-sync-versions', args=[self.pip.pk]), + data=json.dumps(version_post_data), + content_type='application/json', + ) + self.assertEqual(resp.status_code, 200) + + current_stable = self.pip.get_stable_version() + self.assertEqual( + 'origin/stable', + current_stable.identifier + ) + + # Deleting the branch should return the RTD's stable + version_post_data = { + 'branches': [ + { + 'identifier': 'origin/master', + 'verbose_name': 'master', + }, + { + 'identifier': 'origin/0.8.3', + 'verbose_name': '0.8.3', + }, + ], + } + + resp = self.client.post( + reverse('project-sync-versions', args=[self.pip.pk]), + data=json.dumps(version_post_data), + content_type='application/json', + ) + self.assertEqual(resp.status_code, 200) + + # The version 8 should be the new stable. + # The stable isn't stuck with the previous branch + current_stable = self.pip.get_stable_version() + self.assertEqual( + 'origin/0.8.3', + current_stable.identifier + ) + self.assertTrue(current_stable.machine) + + def test_machine_attr_when_user_define_stable_branch_and_delete_it_new_project(self): + """ + The user imports a new project with a branch named ``stable``, + when syncing the versions, the RTD's ``stable`` is lost + (set to machine=False) and doesn't update automatically anymore, + when the branch is deleted on the user repository, the RTD's ``stable`` + is back (set to machine=True). + """ + # There isn't a stable version yet + self.pip.versions.exclude(slug='master').delete() + current_stable = self.pip.get_stable_version() + self.assertIsNone(current_stable) + + version_post_data = { + 'branches': [ + { + 'identifier': 'origin/master', + 'verbose_name': 'master', + }, + # User stable + { + 'identifier': 'origin/stable', + 'verbose_name': 'stable', + }, + { + 'identifier': 'origin/0.8.3', + 'verbose_name': '0.8.3', + }, + ], + } + + resp = self.client.post( + reverse('project-sync-versions', args=[self.pip.pk]), + data=json.dumps(version_post_data), + content_type='application/json', + ) + self.assertEqual(resp.status_code, 200) + + current_stable = self.pip.get_stable_version() + self.assertEqual( + 'origin/stable', + current_stable.identifier + ) + + # User activates the stable version + current_stable.active = True + current_stable.save() + + # Deleting the branch should return the RTD's stable + version_post_data = { + 'branches': [ + { + 'identifier': 'origin/master', + 'verbose_name': 'master', + }, + { + 'identifier': 'origin/0.8.3', + 'verbose_name': '0.8.3', + }, + ], + } + + resp = self.client.post( + reverse('project-sync-versions', args=[self.pip.pk]), + data=json.dumps(version_post_data), + content_type='application/json', + ) + self.assertEqual(resp.status_code, 200) + + # The version 8 should be the new stable. + # The stable isn't stuck with the previous commit + current_stable = self.pip.get_stable_version() + self.assertEqual( + 'origin/0.8.3', + current_stable.identifier + ) + self.assertTrue(current_stable.machine) + + def test_machine_attr_when_user_define_latest_tag_and_delete_it(self): + """ + The user creates a tag named ``latest`` on an existing repo, + when syncing the versions, the RTD's ``latest`` is lost + (set to machine=False) and doesn't update automatically anymore, + when the tag is deleted on the user repository, the RTD's ``latest`` + is back (set to machine=True). + """ + version_post_data = { + 'branches': [ + { + 'identifier': 'origin/master', + 'verbose_name': 'master', + }, + ], + 'tags': [ + # User new stable + { + 'identifier': '1abc2def3', + 'verbose_name': 'latest', + }, + ], + } + + resp = self.client.post( + reverse('project-sync-versions', args=[self.pip.pk]), + data=json.dumps(version_post_data), + content_type='application/json', + ) + self.assertEqual(resp.status_code, 200) + + # The tag is the new latest + version_latest = self.pip.versions.get(slug='latest') + self.assertEqual( + '1abc2def3', + version_latest.identifier + ) + + # Deleting the tag should return the RTD's latest + version_post_data = { + 'branches': [ + { + 'identifier': 'origin/master', + 'verbose_name': 'master', + }, + ], + 'tags': [] + } + + resp = self.client.post( + reverse('project-sync-versions', args=[self.pip.pk]), + data=json.dumps(version_post_data), + content_type='application/json', + ) + self.assertEqual(resp.status_code, 200) + + # The latest isn't stuck with the previous commit + version_latest = self.pip.versions.get(slug='latest') + self.assertEqual( + 'master', + version_latest.identifier + ) + self.assertTrue(version_latest.machine) + + def test_machine_attr_when_user_define_latest_branch_and_delete_it(self): + """ + The user creates a branch named ``latest`` on an existing repo, + when syncing the versions, the RTD's ``latest`` is lost + (set to machine=False) and doesn't update automatically anymore, + when the branch is deleted on the user repository, the RTD's ``latest`` + is back (set to machine=True). + """ + version_post_data = { + 'branches': [ + { + 'identifier': 'origin/master', + 'verbose_name': 'master', + }, + # User new latest + { + 'identifier': 'origin/latest', + 'verbose_name': 'latest', + }, + ], + } + + resp = self.client.post( + reverse('project-sync-versions', args=[self.pip.pk]), + data=json.dumps(version_post_data), + content_type='application/json', + ) + self.assertEqual(resp.status_code, 200) + + # The branch is the new latest + version_latest = self.pip.versions.get(slug='latest') + self.assertEqual( + 'origin/latest', + version_latest.identifier + ) + + # Deleting the branch should return the RTD's latest + version_post_data = { + 'branches': [ + { + 'identifier': 'origin/master', + 'verbose_name': 'master', + }, + ], + } + + resp = self.client.post( + reverse('project-sync-versions', args=[self.pip.pk]), + data=json.dumps(version_post_data), + content_type='application/json', + ) + self.assertEqual(resp.status_code, 200) + + # The latest isn't stuck with the previous branch + version_latest = self.pip.versions.get(slug='latest') + self.assertEqual( + 'master', + version_latest.identifier, + ) + self.assertTrue(version_latest.machine) + class TestStableVersion(TestCase): fixtures = ['eric', 'test_data'] @@ -469,8 +968,95 @@ def test_unicode(self): ) self.assertEqual(resp.status_code, 200) - def test_user_defined_stable_version_with_tags(self): + def test_user_defined_stable_version_tag_with_tags(self): + Version.objects.create( + project=self.pip, + identifier='0.8.3', + verbose_name='0.8.3', + active=True, + ) + + # A pre-existing active stable tag that was machine created + Version.objects.create( + project=self.pip, + identifier='foo', + type=TAG, + verbose_name='stable', + active=True, + machine=True, + ) + + version_post_data = { + 'branches': [ + { + 'identifier': 'origin/master', + 'verbose_name': 'master', + }, + ], + 'tags': [ + # A new user-defined stable tag + { + 'identifier': '1abc2def3', + 'verbose_name': 'stable', + }, + { + 'identifier': '0.9', + 'verbose_name': '0.9', + }, + { + 'identifier': '0.8.3', + 'verbose_name': '0.8.3', + }, + ], + } + + resp = self.client.post( + reverse('project-sync-versions', args=[self.pip.pk]), + data=json.dumps(version_post_data), + content_type='application/json', + ) + self.assertEqual(resp.status_code, 200) + + # Didn't update to newest tag + version_9 = self.pip.versions.get(slug='0.9') + self.assertFalse(version_9.active) + + # Did update to user-defined stable version + version_stable = self.pip.versions.get(slug='stable') + self.assertFalse(version_stable.machine) + self.assertTrue(version_stable.active) + self.assertEqual( + '1abc2def3', + self.pip.get_stable_version().identifier + ) + + # There arent others stable slugs like stable_a + other_stable = self.pip.versions.filter( + slug__startswith='stable_' + ) + self.assertFalse(other_stable.exists()) + + # Check that posting again doesn't change anything from current state. + resp = self.client.post( + reverse('project-sync-versions', args=[self.pip.pk]), + data=json.dumps(version_post_data), + content_type='application/json', + ) + self.assertEqual(resp.status_code, 200) + + version_stable = self.pip.versions.get(slug='stable') + self.assertFalse(version_stable.machine) + self.assertTrue(version_stable.active) + self.assertEqual( + '1abc2def3', + self.pip.get_stable_version().identifier + ) + other_stable = self.pip.versions.filter( + slug__startswith='stable_' + ) + self.assertFalse(other_stable.exists()) + def test_user_defined_stable_version_branch_with_tags(self): Version.objects.create( project=self.pip, identifier='0.8.3', @@ -482,7 +1068,7 @@ def test_user_defined_stable_version_with_tags(self): Version.objects.create( project=self.pip, identifier='foo', - type='branch', + type=BRANCH, verbose_name='stable', active=True, machine=True, @@ -512,27 +1098,185 @@ def test_user_defined_stable_version_with_tags(self): ], } - self.client.post( - '/api/v2/project/{}/sync_versions/'.format(self.pip.pk), + resp = self.client.post( + reverse('project-sync-versions', args=[self.pip.pk]), data=json.dumps(version_post_data), content_type='application/json', ) + self.assertEqual(resp.status_code, 200) # Didn't update to newest tag - version_9 = Version.objects.get(slug='0.9') + version_9 = self.pip.versions.get(slug='0.9') self.assertFalse(version_9.active) # Did update to user-defined stable version - version_stable = Version.objects.get(slug='stable') + version_stable = self.pip.versions.get(slug='stable') self.assertFalse(version_stable.machine) self.assertTrue(version_stable.active) - self.assertEqual('origin/stable', self.pip.get_stable_version().identifier) + self.assertEqual( + 'origin/stable', + self.pip.get_stable_version().identifier + ) + # There arent others stable slugs like stable_a + other_stable = self.pip.versions.filter( + slug__startswith='stable_' + ) + self.assertFalse(other_stable.exists()) # Check that posting again doesn't change anything from current state. - self.client.post( - '/api/v2/project/{}/sync_versions/'.format(self.pip.pk), + resp = self.client.post( + reverse('project-sync-versions', args=[self.pip.pk]), data=json.dumps(version_post_data), content_type='application/json', ) + self.assertEqual(resp.status_code, 200) - self.assertEqual('origin/stable', self.pip.get_stable_version().identifier) + version_stable = self.pip.versions.get(slug='stable') + self.assertFalse(version_stable.machine) + self.assertTrue(version_stable.active) + self.assertEqual( + 'origin/stable', + self.pip.get_stable_version().identifier + ) + other_stable = self.pip.versions.filter( + slug__startswith='stable_' + ) + self.assertFalse(other_stable.exists()) + + +class TestLatestVersion(TestCase): + fixtures = ['eric', 'test_data'] + + def setUp(self): + self.client.login(username='eric', password='test') + self.pip = Project.objects.get(slug='pip') + Version.objects.create( + project=self.pip, + identifier='origin/master', + verbose_name='master', + active=True, + machine=True, + type=BRANCH, + ) + # When the project is saved, the RTD's ``latest`` version + # is created. + self.pip.save() + + def test_user_defined_latest_version_tag(self): + # TODO: the ``latest`` versions are created + # as a BRANCH, then here we will have a + # ``latest_a`` version. + version_post_data = { + 'branches': [ + { + 'identifier': 'origin/master', + 'verbose_name': 'master', + }, + ], + 'tags': [ + # A new user-defined latest tag + { + 'identifier': '1abc2def3', + 'verbose_name': 'latest', + }, + ], + } + + resp = self.client.post( + reverse('project-sync-versions', args=[self.pip.pk]), + data=json.dumps(version_post_data), + content_type='application/json', + ) + self.assertEqual(resp.status_code, 200) + + # Did update to user-defined latest version + version_latest = self.pip.versions.get(slug='latest') + self.assertFalse(version_latest.machine) + self.assertTrue(version_latest.active) + self.assertEqual( + '1abc2def3', + version_latest.identifier + ) + + # There arent others latest slugs like latest_a + other_latest = self.pip.versions.filter( + slug__startswith='latest_' + ) + self.assertFalse(other_latest.exists()) + + # Check that posting again doesn't change anything from current state. + resp = self.client.post( + reverse('project-sync-versions', args=[self.pip.pk]), + data=json.dumps(version_post_data), + content_type='application/json', + ) + self.assertEqual(resp.status_code, 200) + + version_latest = self.pip.versions.get(slug='latest') + self.assertFalse(version_latest.machine) + self.assertTrue(version_latest.active) + self.assertEqual( + '1abc2def3', + version_latest.identifier + ) + other_latest = self.pip.versions.filter( + slug__startswith='latest_' + ) + self.assertFalse(other_latest.exists()) + + def test_user_defined_latest_version_branch(self): + version_post_data = { + 'branches': [ + { + 'identifier': 'origin/master', + 'verbose_name': 'master', + }, + # A new user-defined latest branch + { + 'identifier': 'origin/latest', + 'verbose_name': 'latest', + }, + ], + } + + resp = self.client.post( + reverse('project-sync-versions', args=[self.pip.pk]), + data=json.dumps(version_post_data), + content_type='application/json', + ) + self.assertEqual(resp.status_code, 200) + + # Did update to user-defined latest version + version_latest = self.pip.versions.get(slug='latest') + self.assertFalse(version_latest.machine) + self.assertTrue(version_latest.active) + self.assertEqual( + 'origin/latest', + version_latest.identifier + ) + + # There arent others latest slugs like latest_a + other_latest = self.pip.versions.filter( + slug__startswith='latest_' + ) + self.assertFalse(other_latest.exists()) + + # Check that posting again doesn't change anything from current state. + resp = self.client.post( + reverse('project-sync-versions', args=[self.pip.pk]), + data=json.dumps(version_post_data), + content_type='application/json', + ) + self.assertEqual(resp.status_code, 200) + + version_latest = self.pip.versions.get(slug='latest') + self.assertFalse(version_latest.machine) + self.assertTrue(version_latest.active) + self.assertEqual( + 'origin/latest', + version_latest.identifier + ) + other_latest = self.pip.versions.filter( + slug__startswith='latest_' + ) + self.assertFalse(other_latest.exists()) diff --git a/readthedocs/rtd_tests/utils.py b/readthedocs/rtd_tests/utils.py index 55f7f70b168..7ea3d091cdc 100644 --- a/readthedocs/rtd_tests/utils.py +++ b/readthedocs/rtd_tests/utils.py @@ -1,17 +1,20 @@ """Utility functions for use in tests.""" -from __future__ import absolute_import, unicode_literals +from __future__ import ( + absolute_import, division, print_function, unicode_literals) import logging import subprocess from os import chdir, environ, getcwd, mkdir -from os.path import abspath, join as pjoin +from os.path import abspath +from os.path import join as pjoin from shutil import copytree from tempfile import mkdtemp -from django_dynamic_fixture import new from django.contrib.auth.models import User +from django_dynamic_fixture import new +from readthedocs.doc_builder.base import restoring_chdir log = logging.getLogger(__name__) @@ -92,10 +95,10 @@ def make_test_git(): return directory +@restoring_chdir def create_git_tag(directory, tag, annotated=False): env = environ.copy() env['GIT_DIR'] = pjoin(directory, '.git') - path = getcwd() chdir(directory) command = ['git', 'tag'] @@ -103,7 +106,36 @@ def create_git_tag(directory, tag, annotated=False): command.extend(['-a', '-m', 'Some tag']) command.append(tag) check_output(command, env=env) - chdir(path) + + +@restoring_chdir +def delete_git_tag(directory, tag): + env = environ.copy() + env['GIT_DIR'] = pjoin(directory, '.git') + chdir(directory) + + command = ['git', 'tag', '--delete', tag] + check_output(command, env=env) + + +@restoring_chdir +def create_git_branch(directory, branch): + env = environ.copy() + env['GIT_DIR'] = pjoin(directory, '.git') + chdir(directory) + + command = ['git', 'branch', branch] + check_output(command, env=env) + + +@restoring_chdir +def delete_git_branch(directory, branch): + env = environ.copy() + env['GIT_DIR'] = pjoin(directory, '.git') + chdir(directory) + + command = ['git', 'branch', '-D', branch] + check_output(command, env=env) def make_test_hg():