|
4 | 4 | from re import fullmatch
|
5 | 5 | from urllib.parse import urlparse
|
6 | 6 |
|
| 7 | +from allauth.socialaccount.models import SocialAccount |
7 | 8 | from django import forms
|
8 | 9 | from django.conf import settings
|
9 | 10 | from django.contrib.auth.models import User
|
|
12 | 13 | from django.utils.translation import gettext_lazy as _
|
13 | 14 |
|
14 | 15 | from readthedocs.builds.constants import INTERNAL
|
| 16 | +from readthedocs.core.forms import PrevalidatedForm, RichValidationError |
15 | 17 | from readthedocs.core.history import SimpleHistoryModelForm
|
| 18 | +from readthedocs.core.permissions import AdminPermission |
16 | 19 | from readthedocs.core.utils import slugify, trigger_build
|
17 | 20 | from readthedocs.core.utils.extend import SettingsOverrideObject
|
18 | 21 | from readthedocs.integrations.models import Integration
|
19 | 22 | from readthedocs.invitations.models import Invitation
|
20 | 23 | from readthedocs.oauth.models import RemoteRepository
|
| 24 | +from readthedocs.organizations.models import Team |
21 | 25 | from readthedocs.projects.models import (
|
22 | 26 | AddonsConfig,
|
23 | 27 | Domain,
|
@@ -78,6 +82,112 @@ class ProjectBackendForm(forms.Form):
|
78 | 82 | backend = forms.CharField()
|
79 | 83 |
|
80 | 84 |
|
| 85 | +class ProjectFormPrevalidateMixin: |
| 86 | + |
| 87 | + """Provides shared logic between the automatic and manual create forms.""" |
| 88 | + |
| 89 | + def __init__(self, *args, **kwargs): |
| 90 | + self.user = kwargs.pop("user", None) |
| 91 | + super().__init__(*args, **kwargs) |
| 92 | + |
| 93 | + def clean_prevalidation(self): |
| 94 | + # Shared conditionals between automatic and manual forms |
| 95 | + self.user_has_connected_account = SocialAccount.objects.filter( |
| 96 | + user=self.user, |
| 97 | + ).exists() |
| 98 | + self.user_is_nonowner_with_sso = None |
| 99 | + self.user_missing_admin_permission = None |
| 100 | + if settings.RTD_ALLOW_ORGANIZATIONS: |
| 101 | + # TODO there should be some way to initially select the organization |
| 102 | + # and maybe the team too. It's mostly safe to automatically select |
| 103 | + # the first organization, but explicit would be better. Reusing the |
| 104 | + # organization selection UI works, we only really need a query param |
| 105 | + # here. |
| 106 | + self.user_is_nonowner_with_sso = all( |
| 107 | + [ |
| 108 | + AdminPermission.has_sso_enabled(self.user), |
| 109 | + AdminPermission.organizations( |
| 110 | + user=self.user, |
| 111 | + owner=False, |
| 112 | + ).exists(), |
| 113 | + ] |
| 114 | + ) |
| 115 | + |
| 116 | + # TODO this logic should be possible from AdminPermission |
| 117 | + # AdminPermssion.is_admin only inspects organization owners, so the |
| 118 | + # additional team check is necessary |
| 119 | + self.user_has_admin_permission = any( |
| 120 | + [ |
| 121 | + AdminPermission.organizations( |
| 122 | + user=self.user, |
| 123 | + owner=True, |
| 124 | + ).exists(), |
| 125 | + Team.objects.admin(self.user).exists(), |
| 126 | + ] |
| 127 | + ) |
| 128 | + |
| 129 | + |
| 130 | +class ProjectAutomaticForm(ProjectFormPrevalidateMixin, PrevalidatedForm): |
| 131 | + def clean_prevalidation(self): |
| 132 | + """ |
| 133 | + Block user from using this form for important blocking states. |
| 134 | +
|
| 135 | + We know before the user gets a chance to use this form that the user |
| 136 | + might not have the ability to add a project into their organization. |
| 137 | + These errors are raised before the user submits the form. |
| 138 | + """ |
| 139 | + super().clean_prevalidation() |
| 140 | + if not self.user_has_connected_account: |
| 141 | + url = reverse("socialaccount_connections") |
| 142 | + raise RichValidationError( |
| 143 | + _( |
| 144 | + f"You must first <a href='{url}'>add a connected service " |
| 145 | + f"to your account</a> to enable automatic configuration of " |
| 146 | + f"repositories." |
| 147 | + ), |
| 148 | + header=_("No connected services found"), |
| 149 | + ) |
| 150 | + if settings.RTD_ALLOW_ORGANIZATIONS: |
| 151 | + if self.user_is_nonowner_with_sso: |
| 152 | + raise RichValidationError( |
| 153 | + _( |
| 154 | + "Only organization owners may create new projects " |
| 155 | + "when single sign-on is enabled." |
| 156 | + ), |
| 157 | + header=_("Organization single sign-on enabled"), |
| 158 | + ) |
| 159 | + if not self.user_has_admin_permission: |
| 160 | + raise RichValidationError( |
| 161 | + _( |
| 162 | + "You must be on a team with admin permissions " |
| 163 | + "in order to add a new project to your organization." |
| 164 | + ), |
| 165 | + header=_("Admin permission required"), |
| 166 | + ) |
| 167 | + |
| 168 | + |
| 169 | +class ProjectManualForm(ProjectFormPrevalidateMixin, PrevalidatedForm): |
| 170 | + def clean_prevalidation(self): |
| 171 | + super().clean_prevalidation() |
| 172 | + if settings.RTD_ALLOW_ORGANIZATIONS: |
| 173 | + if self.user_is_nonowner_with_sso: |
| 174 | + raise RichValidationError( |
| 175 | + _( |
| 176 | + "Projects cannot be manually configured when " |
| 177 | + "single sign-on is enabled for your organization." |
| 178 | + ), |
| 179 | + header=_("Organization single sign-on enabled"), |
| 180 | + ) |
| 181 | + if not self.user_has_admin_permission: |
| 182 | + raise RichValidationError( |
| 183 | + _( |
| 184 | + "You must be on a team with admin permissions " |
| 185 | + "in order to add a new project to your organization." |
| 186 | + ), |
| 187 | + header=_("Admin permission required"), |
| 188 | + ) |
| 189 | + |
| 190 | + |
81 | 191 | class ProjectBasicsForm(ProjectForm):
|
82 | 192 |
|
83 | 193 | """Form for basic project fields."""
|
|
0 commit comments