Skip to content

Commit 5c65f16

Browse files
committed
External versions: save state (open / closed)
When an external Version is created, we mark it as `.state="open"` and when it's closed/merged we mark is as `.state="closed"`. Note that we cannot use `.active=False` for this as we were doing, because we want to keep serving closed/merged PRs during some extra time after they are closed/merged and El Proxito is not serving inactive versions. With this approach, external versions will be always `.active=True` but with different `.state=`. That state is used by the periodic Celery task to know if the version has to be deleted or not. Closes #9109
1 parent 2054da8 commit 5c65f16

File tree

9 files changed

+102
-46
lines changed

9 files changed

+102
-46
lines changed

readthedocs/api/v2/views/integrations.py

+14-10
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,15 @@
1515
from rest_framework.status import HTTP_400_BAD_REQUEST
1616
from rest_framework.views import APIView
1717

18+
from readthedocs.builds.constants import (
19+
EXTERNAL_VERSION_STATE_CLOSED,
20+
EXTERNAL_VERSION_STATE_OPEN,
21+
)
1822
from readthedocs.core.signals import webhook_bitbucket, webhook_github, webhook_gitlab
1923
from readthedocs.core.views.hooks import (
2024
build_branches,
2125
build_external_version,
22-
deactivate_external_version,
26+
close_external_version,
2327
get_or_create_external_version,
2428
trigger_sync_versions,
2529
)
@@ -249,14 +253,14 @@ def get_external_version_response(self, project):
249253
"versions": [to_build] if to_build else [],
250254
}
251255

252-
def get_deactivated_external_version_response(self, project):
256+
def get_closed_external_version_response(self, project):
253257
"""
254-
Deactivate the external version on merge/close events and return the API response.
258+
Close the external version on merge/close events and return the API response.
255259
256260
Return a JSON response with the following::
257261
258262
{
259-
"version_deactivated": true,
263+
"closed": true,
260264
"project": "project_name",
261265
"versions": [verbose_name]
262266
}
@@ -265,14 +269,14 @@ def get_deactivated_external_version_response(self, project):
265269
:type project: Project
266270
"""
267271
version_data = self.get_external_version_data()
268-
deactivated_version = deactivate_external_version(
272+
version_closed = close_external_version(
269273
project=project,
270274
version_data=version_data,
271275
)
272276
return {
273-
'version_deactivated': bool(deactivated_version),
274-
'project': project.slug,
275-
'versions': [deactivated_version] if deactivated_version else [],
277+
"closed": bool(version_closed),
278+
"project": project.slug,
279+
"versions": [version_closed] if version_closed else [],
276280
}
277281

278282

@@ -432,7 +436,7 @@ def handle_webhook(self):
432436

433437
if action == GITHUB_PULL_REQUEST_CLOSED:
434438
# Delete external version when PR is closed
435-
return self.get_deactivated_external_version_response(self.project)
439+
return self.get_closed_external_version_response(self.project)
436440

437441
# Sync versions when push event is created/deleted action
438442
if all([
@@ -594,7 +598,7 @@ def handle_webhook(self):
594598

595599
if action in [GITLAB_MERGE_REQUEST_CLOSE, GITLAB_MERGE_REQUEST_MERGE]:
596600
# Handle merge and close merge_request event.
597-
return self.get_deactivated_external_version_response(self.project)
601+
return self.get_closed_external_version_response(self.project)
598602
return None
599603

600604
def _normalize_ref(self, ref):

readthedocs/builds/constants.py

+6
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@
5151
(EXTERNAL, EXTERNAL_TEXT),
5252
(UNKNOWN, UNKNOWN_TEXT),
5353
)
54+
EXTERNAL_VERSION_STATE_OPEN = "open"
55+
EXTERNAL_VERSION_STATE_CLOSED = "closed"
56+
EXTERNAL_VERSION_STATES = (
57+
(EXTERNAL_VERSION_STATE_OPEN, "Open"),
58+
(EXTERNAL_VERSION_STATE_CLOSED, "Closed"),
59+
)
5460

5561
LATEST = settings.RTD_LATEST
5662
LATEST_VERBOSE_NAME = settings.RTD_LATEST_VERBOSE_NAME
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Generated by Django 3.2.12 on 2022-04-21 13:50
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("builds", "0041_track_task_id"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="version",
15+
name="state",
16+
field=models.CharField(
17+
blank=True,
18+
choices=[("open", "Open"), ("closed", "Closed"), ("merged", "Merged")],
19+
help_text="State of the PR/MR associated to this version.",
20+
max_length=20,
21+
null=True,
22+
verbose_name="State",
23+
),
24+
),
25+
]

readthedocs/builds/models.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
BUILD_STATUS_CHOICES,
2828
BUILD_TYPES,
2929
EXTERNAL,
30+
EXTERNAL_VERSION_STATES,
3031
INTERNAL,
3132
LATEST,
3233
NON_REPOSITORY_VERSIONS,
@@ -140,8 +141,16 @@ class Version(TimeStampedModel):
140141

141142
supported = models.BooleanField(_('Supported'), default=True)
142143
active = models.BooleanField(_('Active'), default=False)
143-
built = models.BooleanField(_('Built'), default=False)
144-
uploaded = models.BooleanField(_('Uploaded'), default=False)
144+
state = models.CharField(
145+
_("State"),
146+
max_length=20,
147+
choices=EXTERNAL_VERSION_STATES,
148+
null=True,
149+
blank=True,
150+
help_text=_("State of the PR/MR associated to this version."),
151+
)
152+
built = models.BooleanField(_("Built"), default=False)
153+
uploaded = models.BooleanField(_("Uploaded"), default=False)
145154
privacy_level = models.CharField(
146155
_('Privacy Level'),
147156
max_length=20,

readthedocs/builds/tasks.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
BUILD_STATUS_PENDING,
2323
BUILD_STATUS_SUCCESS,
2424
EXTERNAL,
25+
EXTERNAL_VERSION_STATE_CLOSED,
2526
LOCK_EXPIRE,
2627
MAX_BUILD_COMMAND_SIZE,
2728
TAG,
@@ -228,15 +229,15 @@ def archive_builds_task(self, days=14, limit=200, delete=False):
228229

229230

230231
@app.task(queue='web')
231-
def delete_inactive_external_versions(limit=200, days=30 * 3):
232+
def delete_closed_external_versions(limit=200, days=30 * 3):
232233
"""
233-
Delete external versions that have been marked as inactive after ``days``.
234+
Delete external versions that have been marked as closed after ``days``.
234235
235236
The commit status is updated to link to the build page, as the docs are removed.
236237
"""
237238
days_ago = timezone.now() - timezone.timedelta(days=days)
238239
queryset = Version.external.filter(
239-
active=False,
240+
state=EXTERNAL_VERSION_STATE_CLOSED,
240241
modified__lte=days_ago,
241242
)[:limit]
242243
for version in queryset:

readthedocs/builds/tests/test_tasks.py

+16-10
Original file line numberDiff line numberDiff line change
@@ -5,71 +5,77 @@
55
from django.utils import timezone
66
from django_dynamic_fixture import get
77

8-
from readthedocs.builds.constants import BRANCH, EXTERNAL, TAG
8+
from readthedocs.builds.constants import (
9+
BRANCH,
10+
EXTERNAL,
11+
EXTERNAL_VERSION_STATE_CLOSED,
12+
EXTERNAL_VERSION_STATE_OPEN,
13+
TAG,
14+
)
915
from readthedocs.builds.models import Build, BuildCommandResult, Version
1016
from readthedocs.builds.tasks import (
1117
archive_builds_task,
12-
delete_inactive_external_versions,
18+
delete_closed_external_versions,
1319
)
1420
from readthedocs.projects.models import Project
1521

1622

1723
class TestTasks(TestCase):
1824

19-
def test_delete_inactive_external_versions(self):
25+
def test_delete_closed_external_versions(self):
2026
project = get(Project)
2127
project.versions.all().delete()
2228
get(
2329
Version,
2430
project=project,
2531
slug='branch',
2632
type=BRANCH,
27-
active=False,
33+
state=EXTERNAL_VERSION_STATE_CLOSED,
2834
modified=datetime.now() - timedelta(days=7),
2935
)
3036
get(
3137
Version,
3238
project=project,
3339
slug='tag',
3440
type=TAG,
35-
active=True,
41+
state=EXTERNAL_VERSION_STATE_OPEN,
3642
modified=datetime.now() - timedelta(days=7),
3743
)
3844
get(
3945
Version,
4046
project=project,
4147
slug='external-active',
4248
type=EXTERNAL,
43-
active=True,
49+
state=EXTERNAL_VERSION_STATE_OPEN,
4450
modified=datetime.now() - timedelta(days=7),
4551
)
4652
get(
4753
Version,
4854
project=project,
4955
slug='external-inactive',
5056
type=EXTERNAL,
51-
active=False,
57+
state=EXTERNAL_VERSION_STATE_CLOSED,
5258
modified=datetime.now() - timedelta(days=3),
5359
)
5460
get(
5561
Version,
5662
project=project,
5763
slug='external-inactive-old',
5864
type=EXTERNAL,
59-
active=False,
65+
state=EXTERNAL_VERSION_STATE_CLOSED,
6066
modified=datetime.now() - timedelta(days=7),
6167
)
6268

6369
self.assertEqual(Version.objects.all().count(), 5)
6470
self.assertEqual(Version.external.all().count(), 3)
6571

6672
# We don't have inactive external versions from 9 days ago.
67-
delete_inactive_external_versions(days=9)
73+
delete_closed_external_versions(days=9)
6874
self.assertEqual(Version.objects.all().count(), 5)
6975
self.assertEqual(Version.external.all().count(), 3)
7076

7177
# We have one inactive external versions from 6 days ago.
72-
delete_inactive_external_versions(days=6)
78+
delete_closed_external_versions(days=6)
7379
self.assertEqual(Version.objects.all().count(), 4)
7480
self.assertEqual(Version.external.all().count(), 2)
7581
self.assertFalse(Version.objects.filter(slug='external-inactive-old').exists())

readthedocs/core/views/hooks.py

+18-15
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

33
import structlog
44

5-
from readthedocs.builds.constants import EXTERNAL
5+
from readthedocs.builds.constants import (
6+
EXTERNAL,
7+
EXTERNAL_VERSION_STATE_CLOSED,
8+
EXTERNAL_VERSION_STATE_OPEN,
9+
)
610
from readthedocs.core.utils import trigger_build
711
from readthedocs.projects.models import Feature, Project
812
from readthedocs.projects.tasks.builds import sync_repository_task
@@ -133,7 +137,11 @@ def get_or_create_external_version(project, version_data):
133137
external_version, created = project.versions.get_or_create(
134138
verbose_name=version_data.id,
135139
type=EXTERNAL,
136-
defaults={"identifier": version_data.commit, "active": True},
140+
defaults={
141+
"identifier": version_data.commit,
142+
"active": True,
143+
"state": EXTERNAL_VERSION_STATE_OPEN,
144+
},
137145
)
138146

139147
if created:
@@ -145,8 +153,8 @@ def get_or_create_external_version(project, version_data):
145153
else:
146154
# Identifier will change if there is a new commit to the Pull/Merge Request.
147155
external_version.identifier = version_data.commit
148-
# If the PR was previously closed it was marked as inactive.
149-
external_version.active = True
156+
# If the PR was previously closed it was marked as closed
157+
external_version.state = EXTERNAL_VERSION_STATE_OPEN
150158
external_version.save()
151159
log.info(
152160
'External version updated.',
@@ -156,21 +164,16 @@ def get_or_create_external_version(project, version_data):
156164
return external_version
157165

158166

159-
def deactivate_external_version(project, version_data):
167+
def close_external_version(project, version_data):
160168
"""
161-
Deactivate external versions using `identifier` and `verbose_name`.
169+
Close external versions using `identifier` and `verbose_name`.
162170
163-
if external version does not exist then returns `None`.
164-
165-
We mark the version as inactive,
166-
so another celery task will remove it after some days.
171+
We mark the version's state as `closed` so another celery task will remove
172+
it after some days. If external version does not exist then returns `None`.
167173
168174
:param project: Project instance
169175
:param version_data: A :py:class:`readthedocs.api.v2.views.integrations.ExternalVersionData`
170176
instance.
171-
:param identifier: Commit Hash
172-
:param verbose_name: pull/merge request number
173-
:returns: verbose_name (pull/merge request number).
174177
:rtype: str
175178
"""
176179
external_version = (
@@ -183,10 +186,10 @@ def deactivate_external_version(project, version_data):
183186
)
184187

185188
if external_version:
186-
external_version.active = False
189+
external_version.state = EXTERNAL_VERSION_STATE_CLOSED
187190
external_version.save()
188191
log.info(
189-
'External version marked as inactive.',
192+
"External version marked as closed.",
190193
project_slug=project.slug,
191194
version_slug=external_version.slug,
192195
)

readthedocs/rtd_tests/tests/test_api.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
BUILD_STATE_TRIGGERED,
4242
BUILD_STATUS_DUPLICATED,
4343
EXTERNAL,
44+
EXTERNAL_VERSION_STATE_CLOSED,
4445
LATEST,
4546
)
4647
from readthedocs.builds.models import Build, BuildCommandResult, Version
@@ -1242,8 +1243,8 @@ def test_github_pull_request_closed_event(self, trigger_build, core_trigger_buil
12421243
manager=EXTERNAL
12431244
).get(verbose_name=pull_request_number)
12441245

1245-
# External version should be inactive.
1246-
self.assertFalse(external_version.active)
1246+
self.assertTrue(external_version.active)
1247+
self.assertEqual(external_version.state, EXTERNAL_VERSION_STATE_CLOSED)
12471248
self.assertEqual(resp.status_code, status.HTTP_200_OK)
12481249
self.assertTrue(resp.data['version_deactivated'])
12491250
self.assertEqual(resp.data['project'], self.project.slug)
@@ -1938,8 +1939,8 @@ def test_gitlab_merge_request_close_event(self, trigger_build, core_trigger_buil
19381939
manager=EXTERNAL
19391940
).get(verbose_name=merge_request_number)
19401941

1941-
# External version should be inactive.
1942-
self.assertFalse(external_version.active)
1942+
self.assertTrue(external_version.active)
1943+
self.assertEqual(external_version.state, EXTERNAL_VERSION_STATE_CLOSED)
19431944
self.assertEqual(resp.status_code, status.HTTP_200_OK)
19441945
self.assertTrue(resp.data['version_deactivated'])
19451946
self.assertEqual(resp.data['project'], self.project.slug)
@@ -1983,7 +1984,8 @@ def test_gitlab_merge_request_merge_event(self, trigger_build, core_trigger_buil
19831984
).get(verbose_name=merge_request_number)
19841985

19851986
# external version should be deleted
1986-
self.assertFalse(external_version.active)
1987+
self.assertTrue(external_version.active)
1988+
self.assertEqual(external_version.state, EXTERNAL_VERSION_STATE_CLOSED)
19871989
self.assertEqual(resp.status_code, status.HTTP_200_OK)
19881990
self.assertTrue(resp.data['version_deactivated'])
19891991
self.assertEqual(resp.data['project'], self.project.slug)

readthedocs/settings/base.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ def TEMPLATES(self):
430430
},
431431
},
432432
'every-day-delete-inactive-external-versions': {
433-
'task': 'readthedocs.builds.tasks.delete_inactive_external_versions',
433+
'task': 'readthedocs.builds.tasks.delete_closed_external_versions',
434434
'schedule': crontab(minute=0, hour=1),
435435
'options': {'queue': 'web'},
436436
},

0 commit comments

Comments
 (0)