Skip to content

Commit 4431807

Browse files
committed
Allow users to edit Version's identifier and/or slug
This is an initial POC to test the minimal implementation that we discussed in readthedocs/meta#147 (comment). The main goals here are: - Keep all the user/original values separated from the real `Version` - Do not update edited `Version` with values from VCS - Keep syncing code working properly - Make the minimal changes to prove this idea is possible
1 parent 7fa9fb3 commit 4431807

File tree

4 files changed

+96
-6
lines changed

4 files changed

+96
-6
lines changed

readthedocs/api/v2/utils.py

+10-6
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def sync_versions_to_db(project, versions, type):
2828
2929
- check if user has a ``stable`` / ``latest`` version and disable ours
3030
- update old versions with newer configs (identifier, type, machine)
31+
- take into account ``VersionOverride`` for versions modified by the user
3132
- create new versions that do not exist on DB (in bulk)
3233
- it does not delete versions
3334
@@ -77,17 +78,20 @@ def sync_versions_to_db(project, versions, type):
7778
# Version is correct
7879
continue
7980

80-
# Update slug with new identifier
81-
Version.objects.filter(
81+
# Update slug with new identifier if it differs
82+
v = Version.objects.filter(
8283
project=project,
8384
verbose_name=version_name,
8485
# Always filter by type, a tag and a branch
8586
# can share the same verbose_name.
8687
type=type,
87-
).update(
88-
identifier=version_id,
89-
machine=False,
90-
)
88+
).first()
89+
90+
# Update the version with VCS data only if the version is not
91+
# overridden by the user
92+
if v and not v.active or (not v.override or not v.override.user_identifier):
93+
v.machine = False
94+
v.identifier = version_id
9195

9296
log.info(
9397
"Re-syncing versions: version updated.",

readthedocs/builds/forms.py

+13
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
RegexAutomationRule,
2222
Version,
2323
VersionAutomationRule,
24+
VersionOverride,
2425
)
2526

2627

@@ -30,6 +31,8 @@ class Meta:
3031
states_fields = ["active", "hidden"]
3132
privacy_fields = ["privacy_level"]
3233
fields = (
34+
"slug",
35+
"identifier",
3336
*states_fields,
3437
*privacy_fields,
3538
)
@@ -89,6 +92,16 @@ def _is_default_version(self):
8992
return project.default_version == self.instance.slug
9093

9194
def save(self, commit=True):
95+
# Recover the original data from DB to save it as backup
96+
version = Version.objects.get(pk=self.instance.pk)
97+
VersionOverride.objects.get_or_create(
98+
version=version,
99+
user_slug=self.instance.slug,
100+
user_identifier=self.instance.identifier,
101+
original_slug=version.slug,
102+
original_identifier=version.identifier,
103+
)
104+
92105
obj = super().save(commit=commit)
93106
obj.post_save(was_active=self._was_active)
94107
return obj
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Generated by Django 4.2.16 on 2024-12-02 15:05
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
import django_extensions.db.fields
6+
from django_safemigrate import Safe
7+
8+
9+
class Migration(migrations.Migration):
10+
safe = Safe.before_deploy
11+
12+
dependencies = [
13+
('builds', '0059_add_version_date_index'),
14+
]
15+
16+
operations = [
17+
migrations.CreateModel(
18+
name='VersionOverride',
19+
fields=[
20+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21+
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
22+
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
23+
('original_slug', models.CharField(blank=True, max_length=255, null=True)),
24+
('user_slug', models.CharField(blank=True, max_length=255, null=True)),
25+
('original_identifier', models.CharField(blank=True, max_length=255, null=True)),
26+
('user_identifier', models.CharField(blank=True, max_length=255, null=True)),
27+
('version', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='override', to='builds.version')),
28+
],
29+
options={
30+
'get_latest_by': 'modified',
31+
'abstract': False,
32+
},
33+
),
34+
]

readthedocs/builds/models.py

+39
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,45 @@
8686
log = structlog.get_logger(__name__)
8787

8888

89+
class VersionOverride(TimeStampedModel):
90+
"""
91+
User-modified ``Version`` of a ``Project``.
92+
93+
We use this model to store all the fields the user has override from the
94+
original ``Version`` and also to keep those original values.
95+
96+
This model allows us to perform a re-sync of VCS versions.
97+
"""
98+
99+
version = models.OneToOneField(
100+
"Version",
101+
related_name="override",
102+
on_delete=models.CASCADE,
103+
)
104+
# TODO: add validations to `_slug` fields. We can't use `VersionSlugField`
105+
# because it requires the `populate_from` field that we don't need here.
106+
original_slug = models.CharField(
107+
max_length=255,
108+
null=True,
109+
blank=True,
110+
)
111+
user_slug = models.CharField(
112+
max_length=255,
113+
null=True,
114+
blank=True,
115+
)
116+
original_identifier = models.CharField(
117+
max_length=255,
118+
null=True,
119+
blank=True,
120+
)
121+
user_identifier = models.CharField(
122+
max_length=255,
123+
null=True,
124+
blank=True,
125+
)
126+
127+
89128
class Version(TimeStampedModel):
90129

91130
"""Version of a ``Project``."""

0 commit comments

Comments
 (0)