-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Allow modifying version slugs #4505
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
Conversation
I like the idea. I need to check it in more details and make a proper review. Although, I want to anticipate a question: what would happen if a slug is exactly the same as another identifier? For example, I have the branches With a quick look at the code, I think we don't handle that. We may need to add this check in the validator of the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this 🎉 I think it's something we need to support.
There are some edge cases that we need to think a little more. It would be good to have some tests on the normal flow but also for these weird (and maybe dumb) cases.
readthedocs/builds/forms.py
Outdated
@@ -29,9 +33,22 @@ def __init__(self, instance=None, *args, **kwargs): # noqa | |||
|
|||
class VersionForm(forms.ModelForm): | |||
|
|||
slug = forms.CharField( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: there is a RegexField: https://docs.djangoproject.com/es/1.9/ref/forms/fields/#regexfield
def __init__(self, *args, **kwargs): | ||
super(VersionForm, self).__init__(*args, **kwargs) | ||
if self.instance and self.instance.slug in (LATEST, STABLE): | ||
self.fields['slug'].disabled = True |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Besides making the field gray in the UI, does this validates this field in the backend also?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. However, our CSS doesn't make the field grey.
|
||
log.info('Triggering a build of the moved version') | ||
trigger_build(version.project, version) | ||
|
||
if 'active' in form.changed_data and version.active is False: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would happen if we de-active
a version and change the slug
in the same request?
Since this is triggering async task something weird may happen. Not sure yet, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll check this edge case.
# Rebuild symlinks to the new slug, remove the old slug's symlink | ||
# Latest and Stable would appear here because they are disabled on the form | ||
log.info('Rebuilding symlinks for %s. A version slug changed', version.project) | ||
broadcast(type='app', task=tasks.symlink_project, args=[version.project.pk]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are already calling this on Version.save
which is called from line 157 (form.save()
).
I'm not 100% sure, but I think that if the version is set as active
a build is triggered for that version when it's saved also.
In case of activate
the version and changing the slug
we will be doing this twice. I, think :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are already calling this on Version.save which is called from line 157
Good catch
I'm not 100% sure, but I think that if the version is set as active a build is triggered for that version when it's saved also.
I don't see this anywhere and in my testing this did not occur.
label = version.verbose_name | ||
label = version.slug | ||
if version.slug not in version.identifier: | ||
label += ' ({})'.format(version.identifier) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will show something like my-slug (a1b2c3d4)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This only occurs where a version is not a tag so generally no. Looking a few lines above might clear this up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will look like my-slug (origin/branchname)
typically.
In my tests, there was a unique key on this field. However, it might be best to have a test for this. |
Definitely want to be able to handle conflicting slugs. For my use case:
|
I might have to rethink some of this. There is a unique key on the the project/slug pair. |
Maybe this isn't a huge deal. You could always edit the tag |
- Check for existing versions - Use a RegexField - Double resyncing of symlinks
I made some changes based on feedback. I added a check for existing versions with the same slug and fixed the double syncing of symlinks. |
readthedocs/builds/forms.py
Outdated
version = self.instance | ||
if version: | ||
if Version.objects.filter(project=version.project, slug=slug).exclude( | ||
pk=version.pk).count() > 0: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use .exists()
rather than .count() > 0
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. I feel like I do this all the time.
New changes look good! Besides the comment about the Also, the current tests are failing for some weird reason, I think. |
I can work around unique slugs by renaming problematic ones. It shouldn't come up too often, and I think that next time I'll make the tag That's going to be confusing for users unless it's well documented though. |
Well, it will throw an error message that should make it reasonably obvious: "There is already a version for this project with that slug" |
<form method="post" action="."> | ||
{% csrf_token %} | ||
|
||
<p> | ||
<label>Version control identifier:</label> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
blocktrans here too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably just regular {% trans %}
but yes.
<p> | ||
<label>Version control identifier:</label> | ||
<strong style="color: black">{{ version.identifier }}</strong> | ||
</p> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The close tag is not at the same level
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm +1 on allowing editing versions.
I do worry about the logic that happens when the versions are synced. Will a tag get named 2.0_1
or similar if there already exists a Version named 2.0
in the database? I guess that's not the end of the world, but it definitely could be confusing for users to have the Versions not line up with the VCS elements.
I'm also curious if there are other places where we're using the slug
for something when we really want the identifier
. I looked through the sync_versions
code and it looks OK, so we're probably safe there.
@@ -29,9 +32,34 @@ def __init__(self, instance=None, *args, **kwargs): # noqa | |||
|
|||
class VersionForm(forms.ModelForm): | |||
|
|||
slug = forms.RegexField( | |||
'^{pattern}$'.format(pattern=VERSION_SLUG_REGEX), | |||
max_length=255, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe for DNS this can only contain 63 characters. We should probably enforce this on the model though: https://stackoverflow.com/questions/10552665/names-and-maximum-lengths-of-the-parts-of-a-url
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is some discussion around that here #3464
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#3464 is about project slugs, not version slugs. Correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The regexfield is built to match the model (https://github.com/rtfd/readthedocs.org/blob/8282885/readthedocs/builds/models.py#L78). I'm happy to change it if you think that's appropriate but the two should match in my opinion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, while I think a 255 character version slug is long, we don't use the version for anything related to DNS correct? We use project slugs for that but not versions, correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes! I was confused, project slugs are used as subdomains, versions I don't think so
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, good call. Ignore me :)
This will also end up effectively deleting the old |
It's the same when somebody has a
I don't think we can add a long term redirect because at least in the Pallets use case they plan to repurpose that. Flask for example, has a |
That is currently how it already works. We can edit slugs in the admin, but we generally don't because it's destructive if any URL's exist to the version. I still think if we expose this to users, there's a good chance they will shoot themselves in the foot by renaming a slug that is actively used, and then 404 a bunch of links. I think creating redirects is a good solution to this. Redirects will only fire on 404's, so if the project reuses the slug, they will mostly just get ignored. Though that could also lead to some weird behavior where 404's on the slug will redirect, but not active pages, which isn't great UX either. So I think probably the best thing to do is make sure it's really explicit that it will break links and shouldn't be done with existing versions. |
I originally liked the idea of the ability to have alias and modify version's slug, but it's bringing some potential problems where we should be careful. Adding this as a "staff only" thing it doesn't worth for me. Core team could do this very easily from the Django shell. I did exactly this for the Django folks some days ago at #4519 dversion = Version.objects.create(
slug='2.1.x',
active=True,
project=django,
identifier='origin/stable/2.1.x',
machine=False,
type='branch',
privacy_level='public',
verbose_name='2.1.x',
)
We can do this for Pallets folk also if that's what they need. I'd keep thinking on making this more User Friendly if we have plenty of users with this needings. Otherwise, we can help Pallets folks to onboard by creating these alias manually and keep thinking what's the best way to solve the general problem. |
Well, we need to modify 2 versions because they already have a If we believe the best way forward is to leave modifying version slugs to staff and @davidism is happy with that then I'm happy to close this PR and move forward in that direction. |
I am going to close this PR as I believe we are just going to either rename branches or edit version slugs in the admin to handle this. |
Perhaps we should update this FAQ item to say you can ask us to change it? https://docs.readthedocs.io/en/latest/faq.html#how-do-i-change-my-slug-the-url-your-docs-are-served-at |
@ericholscher that's a good idea, yeah. |
This change allows version slugs to be modified.
Version.slug
(the thing in the URL, mutable) fromVersion.identifier
(version control identifier, immutable). The explanations on theVersion
model are helpful.Fixes #4167
Screenshot
Alternatives
Another approach to this problem is to have an alias (eg. the
readthedocs
branch's docs could be accessed at/en/readthedocs/
or/en/rtd/
). However, after considering this option, I decided that having one location is probably best. If there were multiple paths, in my opinion, one should redirect to the other (eg./en/rtd/
->/en/readthedocs/
). Furthermore, the point of #4167 is to use the alias in favor of the actual version control branch name. I guess we could make the alias the canonical path or make that an option, but it seemed a bit like overengineering.