Skip to content

Commit

Permalink
Allow users to edit Version's identifier and/or slug
Browse files Browse the repository at this point in the history
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
  • Loading branch information
humitos committed Dec 2, 2024
1 parent 7fa9fb3 commit d48a1cc
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 6 deletions.
16 changes: 10 additions & 6 deletions readthedocs/api/v2/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def sync_versions_to_db(project, versions, type):
- check if user has a ``stable`` / ``latest`` version and disable ours
- update old versions with newer configs (identifier, type, machine)
- take into account ``VersionOverride`` for versions modified by the user
- create new versions that do not exist on DB (in bulk)
- it does not delete versions
Expand Down Expand Up @@ -77,17 +78,20 @@ def sync_versions_to_db(project, versions, type):
# Version is correct
continue

# Update slug with new identifier
Version.objects.filter(
# Update slug with new identifier if it differs
v = Version.objects.filter(
project=project,
verbose_name=version_name,
# Always filter by type, a tag and a branch
# can share the same verbose_name.
type=type,
).update(
identifier=version_id,
machine=False,
)
).first()

# Update the version with VCS data only if the version is not
# overridden by the user
if v and not v.active or (not v.override or not v.override.user_identifier):
v.machine = False
v.identifier = version_id

log.info(
"Re-syncing versions: version updated.",
Expand Down
12 changes: 12 additions & 0 deletions readthedocs/builds/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
RegexAutomationRule,
Version,
VersionAutomationRule,
VersionOverride,
)


Expand All @@ -30,6 +31,8 @@ class Meta:
states_fields = ["active", "hidden"]
privacy_fields = ["privacy_level"]
fields = (
"slug",
"identifier",
*states_fields,
*privacy_fields,
)
Expand Down Expand Up @@ -89,6 +92,15 @@ def _is_default_version(self):
return project.default_version == self.instance.slug

def save(self, commit=True):
# Recover the original data from DB to save it as backup
version = Version.objects.get(pk=self.instance.pk)
override, _ = VersionOverride.objects.get_or_create(version=version)
override.user_slug = self.instance.slug
override.user_identifier = self.instance.identifier
override.original_slug = version.slug
override.original_identifier = version.identifier
override.save()

obj = super().save(commit=commit)
obj.post_save(was_active=self._was_active)
return obj
Expand Down
34 changes: 34 additions & 0 deletions readthedocs/builds/migrations/0060_change_slug_identifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 4.2.16 on 2024-12-02 15:05

from django.db import migrations, models
import django.db.models.deletion
import django_extensions.db.fields
from django_safemigrate import Safe


class Migration(migrations.Migration):
safe = Safe.before_deploy

dependencies = [
('builds', '0059_add_version_date_index'),
]

operations = [
migrations.CreateModel(
name='VersionOverride',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
('original_slug', models.CharField(blank=True, max_length=255, null=True)),
('user_slug', models.CharField(blank=True, max_length=255, null=True)),
('original_identifier', models.CharField(blank=True, max_length=255, null=True)),
('user_identifier', models.CharField(blank=True, max_length=255, null=True)),
('version', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='override', to='builds.version')),
],
options={
'get_latest_by': 'modified',
'abstract': False,
},
),
]
39 changes: 39 additions & 0 deletions readthedocs/builds/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,45 @@
log = structlog.get_logger(__name__)


class VersionOverride(TimeStampedModel):
"""
User-modified ``Version`` of a ``Project``.
We use this model to store all the fields the user has override from the
original ``Version`` and also to keep those original values.
This model allows us to perform a re-sync of VCS versions.
"""

version = models.OneToOneField(
"Version",
related_name="override",
on_delete=models.CASCADE,
)
# TODO: add validations to `_slug` fields. We can't use `VersionSlugField`
# because it requires the `populate_from` field that we don't need here.
original_slug = models.CharField(
max_length=255,
null=True,
blank=True,
)
user_slug = models.CharField(
max_length=255,
null=True,
blank=True,
)
original_identifier = models.CharField(
max_length=255,
null=True,
blank=True,
)
user_identifier = models.CharField(
max_length=255,
null=True,
blank=True,
)


class Version(TimeStampedModel):

"""Version of a ``Project``."""
Expand Down

0 comments on commit d48a1cc

Please sign in to comment.