Skip to content
Open
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
17 changes: 16 additions & 1 deletion readthedocs/core/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,10 @@ class RichChoice:
text: str
#: Choice input value
value: str
#: Right floated content for dropdown item
#: Optional content beneath/to the side for dropdown item
description: str
#: Extra content below text and description
extra: str = None
#: Optional image URL for item
image_url: str = None
#: Optional image alt text
Expand All @@ -217,8 +219,21 @@ class RichSelect(forms.Select):
widget=RichSelect(),
choices=[(choice.value, choice)]
)
Attributes used by templates:
use_data_binding
Set up Knockout data-bindings, disable this if using a web component instead.
"""

use_data_binding = True

def __init__(self, attrs=None):
if attrs is None:
attrs = {}
attrs.setdefault("use_data_binding", self.use_data_binding)
super().__init__(attrs)


class FacetField(forms.MultipleChoiceField):
"""
Expand Down
92 changes: 81 additions & 11 deletions readthedocs/projects/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
import dns.name
import dns.resolver
from allauth.socialaccount.models import SocialAccount
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Field
from crispy_forms.layout import Layout
from crispy_forms.layout import MultiField
from django import forms
from django.conf import settings
from django.contrib.auth.models import User
Expand All @@ -17,6 +21,8 @@

from readthedocs.builds.constants import INTERNAL
from readthedocs.core.forms import PrevalidatedForm
from readthedocs.core.forms import RichChoice
from readthedocs.core.forms import RichSelect
from readthedocs.core.forms import RichValidationError
from readthedocs.core.history import SimpleHistoryModelForm
from readthedocs.core.permissions import AdminPermission
Expand All @@ -43,6 +49,22 @@
from readthedocs.redirects.models import Redirect


class ProjectRemoteRepositorySelect(RichSelect):
"""
Rich select for user's remote repository listing

:param can_connect_remote_repository: Is the current user able to connect a remote repository
"""

can_connect_remote_repository = False

def __init__(self, attrs=None):
if attrs is None:
attrs = {}
attrs.setdefault("can_connect_remote_repository", self.can_connect_remote_repository)
super().__init__(attrs)


class ProjectForm(SimpleHistoryModelForm):
"""
Project form.
Expand All @@ -69,14 +91,9 @@ def __init__(self, *args, **kwargs):
empty_value=None,
help_text=self.fields["remote_repository"].help_text,
label=self.fields["remote_repository"].label,
widget=ProjectRemoteRepositorySelect(attrs={"use_data_binding": False}),
)

# The clone URL will be set from the remote repository.
if self.instance.remote_repository and not self.instance.has_feature(
Feature.DONT_SYNC_WITH_REMOTE_REPO
):
self.fields["repo"].disabled = True

def _get_remote_repository_choices(self):
"""
Get valid choices for the remote repository field.
Expand All @@ -94,14 +111,39 @@ def _get_remote_repository_choices(self):
"""
queryset = RemoteRepository.objects.for_project_linking(self.user)
current_remote_repo = self.instance.remote_repository if self.instance.pk else None
options = [
(None, _("No connected repository")),
choices = [
(
None,
RichChoice(
text=_("No connected repository"),
description=_("This project uses a manually configured repository URL"),
value=None,
),
),
]
# Show currently connected remote repository instance even if the
# maintainer does not control this remote repository through their
# connected account.
if current_remote_repo and current_remote_repo not in queryset:
options.append((current_remote_repo.pk, str(current_remote_repo)))
choice = RichChoice(
text=current_remote_repo.full_name,
description=current_remote_repo.clone_url,
extra=_("This repository is connected through another user's account"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this something users need to know. Like they may feel like there is something they need to do.

value=current_remote_repo.pk,
image_url=current_remote_repo.avatar_url,
)
choices.append((choice.value, choice))

for repo in queryset:
choice = RichChoice(
text=repo.full_name,
description=repo.clone_url,
value=repo.pk,
image_url=repo.avatar_url,
)
choices.append((choice.value, choice))

options.extend((repo.pk, repo.clone_url) for repo in queryset)
return options
return choices

def save(self, commit=True):
project = super().save(commit)
Expand Down Expand Up @@ -510,6 +552,34 @@ def __init__(self, *args, **kwargs):

self.setup_external_builds_option()

# We use crispy layout here strictly for multifield and field ordering,
# There's no HTML in Python, it's all in templates and web components.
self.helper = FormHelper()
# Let templates close form tag and add submit button
self.helper.form_tag = False

multifield_attrs = {}
if not SocialAccount.objects.filter(user=self.user).exists():
multifield_attrs["show-connected-service-warning"] = True
if self.instance.has_feature(Feature.DONT_SYNC_WITH_REMOTE_REPO):
multifield_attrs["dont-sync"] = True
multifield = MultiField(
_("Repository"),
Field("remote_repository"),
Field("repo"),
template="projects/includes/crispy/repository.html",
**multifield_attrs,
)

# We only care about the order of the first fields, the rest of the
# fields are dictated by Meta class configuration.
fields_other = [
field
for field in self.fields.keys()
if field not in ["name", "repo", "remote_repository"]
]
self.helper.layout = Layout("name", multifield, *fields_other)

def clean_readthedocs_yaml_path(self):
"""
Validate user input to help user.
Expand Down
5 changes: 3 additions & 2 deletions readthedocs/rtd_tests/tests/test_project_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,9 +402,10 @@ def test_set_remote_repository(self):
self.assertEqual(self.project.remote_repository, remote_repository)
self.assertEqual(self.project.repo, remote_repository.clone_url)

# Since a remote repository is attached, the repo field should be disabled.
# Since a remote repository is attached, we used to disable the repo
# field. This is now handled more on the front end instead.
form = UpdateProjectForm(data, instance=self.project, user=self.user)
self.assertTrue(form.fields["repo"].disabled)
self.assertFalse(form.fields["repo"].disabled)

# This project has the don't sync with remote repository feature enabled,
# so the repo field should be enabled.
Expand Down