Repository: readthedocs/readthedocs.org

Issue:
Error in `Versions` dashboard when a project has many versions
When a user has many versions and edits the version from the `Versions` menu in the admin panel, they can hit this problem

`The number of GET/POST parameters exceeded settings.DATA_UPLOAD_MAX_NUMBER_FIELDS.` 

Sentry issue https://sentry.io/organizations/read-the-docs/issues/790916614/?project=148442&referrer=RegressionActivityEmail

From our docs project https://readthedocs.org/dashboard/docs/versions/

We can get around setting that to `None`, but we'll lose the security protection.

I think we could remove that menu from there. Some pros/cons

- It feels weird to paginate the versions to edit them (with the current UI at least?)
- The list is very large, and already have the `Versions` menu in the nav bar (which is paginated)
- The current versions list in the admin menu allows users to activate/deactivate  change privacy in several versions at once (we will lose this if we remove it).
- Is this really used anyway? like editing several versions is something users do really often?


Discussion:
I have ran into this issue in the django admin panel also while trying to edit a project.
The versions menu on the admin panel is almost similar to the version menu on the navbar which seems much more efficient to handle version editing. and for the admin panel version page if the project has a lot of versions it creates a very long list of forms which feels less and less usable to my eyes.
> I have ran into this issue in the django admin panel also while trying to edit a project.

Happens with me also.

> The versions menu on the admin panel is almost similar to the version menu on the navbar which seems much more efficient to handle version editing.

I am +1 with @saadmk11 here.

I am +1 on removing the menu from there because
- It is creating problems
- The long list doesn't look good to eyes
- We have similar implementation in `readthedocs.org/projects/<slug>/versions/`

> Is this really used anyway? like editing several versions is something users do really often?

I don't think that users edit 9-10 versions at a single time.

Also I think that we can set `DATA_UPLOAD_MAX_NUMBER_FIELDS` to `None` in development environment only as it creates problem when using django admin panel.

> I think we could remove that menu from there.

:+1: Definitely

> Is this really used anyway? like editing several versions is something users do really often?

It's kind of annoying. I checked GA and it does ~1,500 pageviews/month which is ~0.1%. I think we can kill it.
@davidfischer 
Can I start working on this?
> Can I start working on this?

Yes please.

> Also, what is your opinion on setting DATA_UPLOAD_MAX_NUMBER_FIELDS to None in development environment?

I would rather leave this on in development as it catches a type of problem. It forces us to not ever show 1,000+ fields at a time which is definitely broken UX. Also, I think it is more secure in production. Feel free to override it in your `local_settings.py` file.
@davidfischer 
Thank you for the information.
Will make a PR soon.

Resolving patch:
diff --git a/readthedocs/projects/forms.py b/readthedocs/projects/forms.py
--- a/readthedocs/projects/forms.py
+++ b/readthedocs/projects/forms.py
@@ -465,47 +465,6 @@ def save_version(self, version):
             trigger_build(project=self.project, version=version)
 
 
-def build_versions_form(project):
-    """Versions form with a list of versions and version privacy levels."""
-    attrs = {
-        'project': project,
-    }
-    versions_qs = project.versions.all()  # Admin page, so show all versions
-    active = versions_qs.filter(active=True)
-    if active.exists():
-        active = sort_version_aware(active)
-        choices = [(version.slug, version.verbose_name) for version in active]
-        attrs['default-version'] = forms.ChoiceField(
-            label=_('Default Version'),
-            choices=choices,
-            initial=project.get_default_version(),
-        )
-    versions_qs = sort_version_aware(versions_qs)
-    for version in versions_qs:
-        field_name = 'version-{}'.format(version.slug)
-        privacy_name = 'privacy-{}'.format(version.slug)
-        if version.type == TAG:
-            label = '{} ({})'.format(
-                version.verbose_name,
-                version.identifier[:8],
-            )
-        else:
-            label = version.verbose_name
-        attrs[field_name] = forms.BooleanField(
-            label=label,
-            widget=DualCheckboxWidget(version),
-            initial=version.active,
-            required=False,
-        )
-        attrs[privacy_name] = forms.ChoiceField(
-            # This isn't a real label, but just a slug for the template
-            label='privacy',
-            choices=constants.PRIVACY_CHOICES,
-            initial=version.privacy_level,
-        )
-    return type(str('VersionsForm'), (BaseVersionsForm,), attrs)
-
-
 class BaseUploadHTMLForm(forms.Form):
     content = forms.FileField(label=_('Zip file of HTML'))
     overwrite = forms.BooleanField(
diff --git a/readthedocs/projects/urls/private.py b/readthedocs/projects/urls/private.py
--- a/readthedocs/projects/urls/private.py
+++ b/readthedocs/projects/urls/private.py
@@ -64,10 +64,6 @@
         r'^(?P<project_slug>[-\w]+)/version/(?P<version_slug>[^/]+)/$',
         private.project_version_detail, name='project_version_detail',
     ),
-    url(
-        r'^(?P<project_slug>[-\w]+)/versions/$', private.project_versions,
-        name='projects_versions',
-    ),
     url(
         r'^(?P<project_slug>[-\w]+)/delete/$', private.project_delete,
         name='projects_delete',
diff --git a/readthedocs/projects/views/private.py b/readthedocs/projects/views/private.py
--- a/readthedocs/projects/views/private.py
+++ b/readthedocs/projects/views/private.py
@@ -47,7 +47,6 @@
     UpdateProjectForm,
     UserForm,
     WebHookForm,
-    build_versions_form,
 )
 from readthedocs.projects.models import (
     Domain,
@@ -157,39 +156,6 @@ def get_success_url(self):
         return reverse('projects_detail', args=[self.object.slug])
 
 
-@login_required
-def project_versions(request, project_slug):
-    """
-    Project versions view.
-
-    Shows the available versions and lets the user choose which ones he would
-    like to have built.
-    """
-    project = get_object_or_404(
-        Project.objects.for_admin_user(request.user),
-        slug=project_slug,
-    )
-
-    if not project.is_imported:
-        raise Http404
-
-    form_class = build_versions_form(project)
-
-    form = form_class(data=request.POST or None)
-
-    if request.method == 'POST' and form.is_valid():
-        form.save()
-        messages.success(request, _('Project versions updated'))
-        project_dashboard = reverse('projects_detail', args=[project.slug])
-        return HttpResponseRedirect(project_dashboard)
-
-    return render(
-        request,
-        'projects/project_versions.html',
-        {'form': form, 'project': project},
-    )
-
-
 @login_required
 def project_version_detail(request, project_slug, version_slug):
     """Project version detail page."""


Test patch:
diff --git a/readthedocs/rtd_tests/tests/test_views.py b/readthedocs/rtd_tests/tests/test_views.py
--- a/readthedocs/rtd_tests/tests/test_views.py
+++ b/readthedocs/rtd_tests/tests/test_views.py
@@ -50,8 +50,6 @@ def test_imported_docs(self):
         _ = form.save()
         _ = Project.objects.get(slug='django-kong')
 
-        r = self.client.get('/dashboard/django-kong/versions/', {})
-        self.assertEqual(r.status_code, 200)
         r = self.client.get('/projects/django-kong/builds/')
         self.assertEqual(r.status_code, 200)
         r = self.client.get('/dashboard/django-kong/edit/', {})
@@ -105,10 +103,6 @@ def test_version_detail(self):
         response = self.client.get('/dashboard/pip/version/0.8.1/')
         self.assertRedirectToLogin(response)
 
-    def test_versions(self):
-        response = self.client.get('/dashboard/pip/versions/')
-        self.assertRedirectToLogin(response)
-
     def test_project_delete(self):
         response = self.client.get('/dashboard/pip/delete/')
         self.assertRedirectToLogin(response)