Skip to content

Commit 664a27a

Browse files
committed
Builds: don't delete them when a version is deleted
We lose information when a version is deleted. Setting the version to null allow us to keep the builds around. But now we need to save more data to be able to reproduce some links. A data migration is needed to update old builds, but isn't required, as we fallback to the value from the version. Close #7674
1 parent af3bf8c commit 664a27a

File tree

6 files changed

+180
-71
lines changed

6 files changed

+180
-71
lines changed

readthedocs/api/v2/serializers.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,8 @@ class BuildSerializer(serializers.ModelSerializer):
123123

124124
commands = BuildCommandSerializer(many=True, read_only=True)
125125
project_slug = serializers.ReadOnlyField(source='project.slug')
126-
version_slug = serializers.ReadOnlyField(source='version.slug')
127-
docs_url = serializers.ReadOnlyField(source='version.get_absolute_url')
126+
version_slug = serializers.ReadOnlyField(source='get_version_slug')
127+
docs_url = serializers.SerializerMethodField()
128128
state_display = serializers.ReadOnlyField(source='get_state_display')
129129
commit_url = serializers.ReadOnlyField(source='get_commit_url')
130130
# Jsonfield needs an explicit serializer
@@ -136,6 +136,11 @@ class Meta:
136136
# `_config` should be excluded to avoid conflicts with `config`
137137
exclude = ('builder', '_config')
138138

139+
def get_docs_url(self, obj):
140+
if obj.version:
141+
return obj.version.get_absolute_url()
142+
return ''
143+
139144

140145
class BuildAdminSerializer(BuildSerializer):
141146

readthedocs/api/v3/serializers.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,16 @@ def get__self(self, obj):
6969
return self._absolute_url(path)
7070

7171
def get_version(self, obj):
72-
path = reverse(
73-
'projects-versions-detail',
74-
kwargs={
75-
'parent_lookup_project__slug': obj.project.slug,
76-
'version_slug': obj.version.slug,
77-
},
78-
)
79-
return self._absolute_url(path)
72+
if obj.version:
73+
path = reverse(
74+
'projects-versions-detail',
75+
kwargs={
76+
'parent_lookup_project__slug': obj.project.slug,
77+
'version_slug': obj.version.slug,
78+
},
79+
)
80+
return self._absolute_url(path)
81+
return ''
8082

8183
def get_project(self, obj):
8284
path = reverse(
@@ -103,14 +105,16 @@ def get_project(self, obj):
103105
return self._absolute_url(path)
104106

105107
def get_version(self, obj):
106-
path = reverse(
107-
'project_version_detail',
108-
kwargs={
109-
'project_slug': obj.project.slug,
110-
'version_slug': obj.version.slug
111-
}
112-
)
113-
return self._absolute_url(path)
108+
if obj.version:
109+
path = reverse(
110+
'project_version_detail',
111+
kwargs={
112+
'project_slug': obj.project.slug,
113+
'version_slug': obj.version.slug
114+
}
115+
)
116+
return self._absolute_url(path)
117+
return ""
114118

115119

116120
class BuildConfigSerializer(FlexFieldsSerializerMixin, serializers.Serializer):
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Generated by Django 2.2.16 on 2020-11-18 21:52
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('builds', '0029_add_time_fields'),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name='build',
16+
name='version_name',
17+
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Version name'),
18+
),
19+
migrations.AddField(
20+
model_name='build',
21+
name='version_slug',
22+
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Version slug'),
23+
),
24+
migrations.AddField(
25+
model_name='build',
26+
name='version_type',
27+
field=models.CharField(blank=True, choices=[('branch', 'Branch'), ('tag', 'Tag'), ('external', 'External'), ('unknown', 'Unknown')], max_length=32, null=True, verbose_name='Version type'),
28+
),
29+
migrations.AlterField(
30+
model_name='build',
31+
name='version',
32+
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='builds', to='builds.Version', verbose_name='Version'),
33+
),
34+
]

readthedocs/builds/models.py

Lines changed: 75 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
get_bitbucket_username_repo,
6363
get_github_username_repo,
6464
get_gitlab_username_repo,
65+
get_vcs_url,
6566
)
6667
from readthedocs.builds.version_slug import VersionSlugField
6768
from readthedocs.config import LATEST_CONFIGURATION_VERSION
@@ -73,12 +74,10 @@
7374
GITHUB_BRAND,
7475
GITHUB_COMMIT_URL,
7576
GITHUB_PULL_REQUEST_COMMIT_URL,
76-
GITHUB_PULL_REQUEST_URL,
7777
GITHUB_URL,
7878
GITLAB_BRAND,
7979
GITLAB_COMMIT_URL,
8080
GITLAB_MERGE_REQUEST_COMMIT_URL,
81-
GITLAB_MERGE_REQUEST_URL,
8281
GITLAB_URL,
8382
MEDIA_TYPES,
8483
PRIVACY_CHOICES,
@@ -212,47 +211,22 @@ def ref(self):
212211

213212
@property
214213
def vcs_url(self):
215-
"""
216-
Generate VCS (github, gitlab, bitbucket) URL for this version.
217-
218-
Example: https://github.com/rtfd/readthedocs.org/tree/3.4.2/.
219-
External Version Example: https://github.com/rtfd/readthedocs.org/pull/99/.
220-
"""
221-
if self.type == EXTERNAL:
222-
if 'github' in self.project.repo:
223-
user, repo = get_github_username_repo(self.project.repo)
224-
return GITHUB_PULL_REQUEST_URL.format(
225-
user=user,
226-
repo=repo,
227-
number=self.verbose_name,
228-
)
229-
if 'gitlab' in self.project.repo:
230-
user, repo = get_gitlab_username_repo(self.project.repo)
231-
return GITLAB_MERGE_REQUEST_URL.format(
232-
user=user,
233-
repo=repo,
234-
number=self.verbose_name,
235-
)
236-
# TODO: Add VCS URL for BitBucket.
237-
return ''
238-
239-
url = ''
240-
if self.slug == STABLE:
241-
slug_url = self.ref
242-
elif self.slug == LATEST:
243-
slug_url = self.project.get_default_branch()
244-
else:
245-
slug_url = self.slug
246-
247-
if ('github' in self.project.repo) or ('gitlab' in self.project.repo):
248-
url = f'/tree/{slug_url}/'
249-
250-
if 'bitbucket' in self.project.repo:
251-
slug_url = self.identifier
252-
url = f'/src/{slug_url}'
253-
254-
# TODO: improve this replacing
255-
return self.project.repo.replace('git://', 'https://').replace('.git', '') + url
214+
version_name = self.verbose_name
215+
if not self.is_external:
216+
if self.slug == STABLE:
217+
version_name = self.ref
218+
elif self.slug == LATEST:
219+
version_name = self.project.get_default_branch()
220+
else:
221+
version_name = self.slug
222+
if 'bitbucket' in self.project.repo:
223+
version_name = self.identifier
224+
225+
return get_vcs_url(
226+
project=self.project,
227+
version_type=self.type,
228+
version_name=version_name,
229+
)
256230

257231
@property
258232
def last_build(self):
@@ -623,7 +597,7 @@ class Build(models.Model):
623597
verbose_name=_('Version'),
624598
null=True,
625599
related_name='builds',
626-
on_delete=models.CASCADE,
600+
on_delete=models.SET_NULL,
627601
)
628602
type = models.CharField(
629603
_('Type'),
@@ -662,12 +636,33 @@ class Build(models.Model):
662636
output = models.TextField(_('Output'), default='', blank=True)
663637
error = models.TextField(_('Error'), default='', blank=True)
664638
exit_code = models.IntegerField(_('Exit code'), null=True, blank=True)
639+
640+
# Metadata from were the build happened.
665641
commit = models.CharField(
666642
_('Commit'),
667643
max_length=255,
668644
null=True,
669645
blank=True,
670646
)
647+
version_slug = models.CharField(
648+
_('Version slug'),
649+
max_length=255,
650+
null=True,
651+
blank=True,
652+
)
653+
version_name = models.CharField(
654+
_('Version name'),
655+
max_length=255,
656+
null=True,
657+
blank=True,
658+
)
659+
version_type = models.CharField(
660+
_('Version type'),
661+
max_length=32,
662+
choices=VERSION_TYPES,
663+
null=True,
664+
blank=True,
665+
)
671666
_config = JSONField(_('Configuration used in the build'), default=dict)
672667

673668
length = models.IntegerField(_('Build Length'), null=True, blank=True)
@@ -772,6 +767,11 @@ def save(self, *args, **kwargs): # noqa
772767
# yapf: enable
773768
previous_pk = previous._config.get(self.CONFIG_KEY, previous.pk)
774769
self._config = {self.CONFIG_KEY: previous_pk}
770+
771+
if self.version:
772+
self.version_name = self.version.verbose_name
773+
self.version_slug = self.version.slug
774+
self.version_type = self.version.type
775775
super().save(*args, **kwargs)
776776
self._config_changed = False
777777

@@ -803,6 +803,31 @@ def get_full_url(self):
803803
)
804804
return full_url
805805

806+
def get_version_name(self):
807+
if self.version:
808+
return self.version.verbose_name
809+
return self.version_name
810+
811+
def get_version_slug(self):
812+
if self.version:
813+
return self.version.verbose_name
814+
return self.version_name
815+
816+
def get_version_type(self):
817+
if self.version:
818+
return self.version.type
819+
return self.version_type
820+
821+
@property
822+
def vcs_url(self):
823+
if self.version:
824+
return self.version.vcs_url
825+
return get_vcs_url(
826+
project=self.project,
827+
version_type=self.get_version_type(),
828+
version_name=self.get_version_name(),
829+
)
830+
806831
def get_commit_url(self):
807832
"""Return the commit URL."""
808833
repo_url = self.project.repo
@@ -815,7 +840,7 @@ def get_commit_url(self):
815840
return GITHUB_PULL_REQUEST_COMMIT_URL.format(
816841
user=user,
817842
repo=repo,
818-
number=self.version.verbose_name,
843+
number=self.get_version_name(),
819844
commit=self.commit
820845
)
821846
if 'gitlab' in repo_url:
@@ -826,7 +851,7 @@ def get_commit_url(self):
826851
return GITLAB_MERGE_REQUEST_COMMIT_URL.format(
827852
user=user,
828853
repo=repo,
829-
number=self.version.verbose_name,
854+
number=self.get_version_name(),
830855
commit=self.commit
831856
)
832857
# TODO: Add External Version Commit URL for BitBucket.
@@ -877,7 +902,10 @@ def is_stale(self):
877902

878903
@property
879904
def is_external(self):
880-
return self.version.type == EXTERNAL
905+
type = self.version_type
906+
if self.version:
907+
type = self.version.type
908+
return type == EXTERNAL
881909

882910
@property
883911
def external_version_name(self):

readthedocs/builds/utils.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
from celery.five import monotonic
66
from django.core.cache import cache
77

8+
from readthedocs.builds.constants import EXTERNAL
89
from readthedocs.projects.constants import (
910
BITBUCKET_REGEXS,
11+
GITHUB_PULL_REQUEST_URL,
1012
GITHUB_REGEXS,
13+
GITLAB_MERGE_REQUEST_URL,
1114
GITLAB_REGEXS,
1215
)
1316

@@ -41,6 +44,41 @@ def get_gitlab_username_repo(url=None):
4144
return (None, None)
4245

4346

47+
def get_vcs_url(*, project, version_type, version_name):
48+
"""
49+
Generate VCS (github, gitlab, bitbucket) URL for this version.
50+
51+
Example: https://github.com/rtfd/readthedocs.org/tree/3.4.2/.
52+
External version example: https://github.com/rtfd/readthedocs.org/pull/99/.
53+
"""
54+
if version_type == EXTERNAL:
55+
if 'github' in project.repo:
56+
user, repo = get_github_username_repo(project.repo)
57+
return GITHUB_PULL_REQUEST_URL.format(
58+
user=user,
59+
repo=repo,
60+
number=version_name,
61+
)
62+
if 'gitlab' in project.repo:
63+
user, repo = get_gitlab_username_repo(project.repo)
64+
return GITLAB_MERGE_REQUEST_URL.format(
65+
user=user,
66+
repo=repo,
67+
number=version_name,
68+
)
69+
# TODO: Add VCS URL for BitBucket.
70+
return ''
71+
72+
url = ''
73+
if ('github' in project.repo) or ('gitlab' in project.repo):
74+
url = f'/tree/{version_name}/'
75+
elif 'bitbucket' in project.repo:
76+
url = f'/src/{version_name}'
77+
78+
# TODO: improve this replacing
79+
return project.repo.replace('git://', 'https://').replace('.git', '') + url
80+
81+
4482
@contextmanager
4583
def memcache_lock(lock_id, oid):
4684
"""

0 commit comments

Comments
 (0)