Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow users to edit Version's identifier and/or slug #11814

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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