Skip to content

Organizations: Organization chooser page #10325

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

Merged
merged 19 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9c4ba36
Initial work for an all-purporse URL scheme for organization-specific…
benjaoming May 18, 2023
636445f
Split out handling of querystring
benjaoming May 18, 2023
ce0a05c
Adds organization chooser step (>1 organization), use `-` as "unspeci…
benjaoming May 22, 2023
e8d988e
Do not handle additional kwargs, it's meaningless. Raise 404 and log …
benjaoming May 22, 2023
714de9d
Handle 'next_name' kwarg in tests, handle NoReverseMatch by raising 404
benjaoming May 22, 2023
ebeeed8
Setup initial test cases
benjaoming May 22, 2023
632384f
Test case to ensure that the chooser list is empty when there's no or…
benjaoming May 22, 2023
8e00783
Add further test assertions
benjaoming May 22, 2023
65f385f
Update readthedocs/organizations/templates/organizations/organization…
benjaoming May 23, 2023
27c659d
Revert leak from local dev env
benjaoming May 23, 2023
8c0a258
Organization queryset is already handled in list view
benjaoming May 23, 2023
400362d
Click=>Select + mark string for translation
benjaoming May 23, 2023
82fcffe
Remove <br>, use blocktrans
benjaoming May 23, 2023
2d89f39
Shorten heading
benjaoming May 23, 2023
15c428e
Remove generic URL feature. Note: The removal is kept in a single com…
benjaoming May 24, 2023
1c3bc9e
Merge branch 'main' of github.com:readthedocs/readthedocs.org into or…
benjaoming May 24, 2023
2673c74
Merge branch 'main' of github.com:readthedocs/readthedocs.org into or…
benjaoming May 25, 2023
1594b20
Overwrite get instead of dispatch
benjaoming May 25, 2023
68e893f
Merge branch 'organization-chooser-redirect' of github.com:readthedoc…
benjaoming May 25, 2023
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
2 changes: 1 addition & 1 deletion common
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{% extends "base.html" %}

{% load i18n %}
{% load gravatar %}
{% load organizations %}

{% block title %}{% trans "Choose an organization" %}{% endblock %}

{% block organization-bar-organization %}active{% endblock %}

{% block content %}
<div class="col-major organization-major">
<div class="module organizations organizations-list">
<div class="module-wrapper">

<h2>{% trans "Choose an organization" %}</h2>

<p>
{% blocktrans trimmed %}
You are a member of several organizations.
Select the organization that you want to use:
{% endblocktrans %}
</p>

{% if organization_list %}
<!-- BEGIN organization list -->
<div class="module-list">
<div class="module-list-wrapper">

<ul>
{% for org in organization_list %}
<li>
<a href="{% url next_name slug=org.slug %}{% if next_querystring %}?{{ next_querystring }}{% endif %}">
{% gravatar org.email 48 %}
{{ org.name }}
</a>
</li>
{% endfor %}
</ul>

</div>
</div>
<!-- END organization list -->
{% else %}
<p>{% trans "You aren't currently a member of any organizations." %}</p>
{% endif %}
</div>
</div>
</div>

{% endblock %}
11 changes: 6 additions & 5 deletions readthedocs/organizations/tests/test_privacy_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ def setUp(self):
self.invite = get(TeamInvite, organization=self.organization, team=self.team)
self.default_kwargs.update(
{
'slug': self.organization.slug,
'team': self.team.slug,
'hash': self.invite.hash,
'owner': self.org_owner.pk,
'member': self.team_member.pk,
"slug": self.organization.slug,
"team": self.team.slug,
"hash": self.invite.hash,
"owner": self.org_owner.pk,
"member": self.team_member.pk,
"next_name": "organization_detail",
}
)

Expand Down
126 changes: 126 additions & 0 deletions readthedocs/organizations/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,3 +376,129 @@ def test_organization_signup(self):
resp,
reverse('organization_detail', kwargs={'slug': org.slug}),
)


@override_settings(
RTD_ALLOW_ORGANIZATIONS=True,
RTD_DEFAULT_FEATURES={
TYPE_AUDIT_LOGS: 90,
},
)
class OrganizationUnspecifiedChooser(TestCase):
def setUp(self):
self.owner = get(User, username="owner")
self.member = get(User, username="member")
self.project = get(Project, slug="project")
self.organization = get(
Organization,
owners=[self.owner],
projects=[self.project],
)
self.team = get(
Team,
organization=self.organization,
members=[self.member],
)
self.another_organization = get(
Organization,
owners=[self.owner],
projects=[self.project],
)
self.client.force_login(self.owner)

def test_members_list_redirects_to_organization_choose(self):
self.assertEqual(Organization.objects.count(), 2)
resp = self.client.get(reverse("organization_members", kwargs={"slug": "-"}))
self.assertRedirects(
resp,
reverse(
"organization_choose", kwargs={"next_name": "organization_members"}
),
)

def test_choose_organization_edit(self):
"""
Verify that the choose page works.
"""
self.assertEqual(Organization.objects.count(), 2)
resp = self.client.get(
reverse("organization_choose", kwargs={"next_name": "organization_edit"})
)
self.assertEqual(resp.status_code, 200)
self.assertContains(
resp, reverse("organization_edit", kwargs={"slug": self.organization.slug})
)
self.assertContains(
resp,
reverse(
"organization_edit", kwargs={"slug": self.another_organization.slug}
),
)

def test_no_redirect_organization_team_detail(self):
"""
Verify that organization views w extra URL kwargs just return 404 if unspecified slug.
"""
resp = self.client.get(
reverse(
"organization_team_detail", kwargs={"slug": "-", "team": self.team.pk}
)
)
self.assertEqual(resp.status_code, 404)


@override_settings(
RTD_ALLOW_ORGANIZATIONS=True,
RTD_DEFAULT_FEATURES={
TYPE_AUDIT_LOGS: 90,
},
)
class OrganizationUnspecifiedSingleOrganizationRedirect(TestCase):
def setUp(self):
self.owner = get(User, username="owner")
self.member = get(User, username="member")
self.project = get(Project, slug="project")
self.organization = get(
Organization,
owners=[self.owner],
projects=[self.project],
)
self.client.force_login(self.owner)

def test_unspecified_slug_redirects_to_organization_edit(self):
self.assertEqual(Organization.objects.count(), 1)
resp = self.client.get(reverse("organization_edit", kwargs={"slug": "-"}))
self.assertRedirects(
resp, reverse("organization_edit", kwargs={"slug": self.organization.slug})
)


@override_settings(
RTD_ALLOW_ORGANIZATIONS=True,
RTD_DEFAULT_FEATURES={
TYPE_AUDIT_LOGS: 90,
},
)
class OrganizationUnspecifiedNoOrganizationRedirect(TestCase):
def setUp(self):
self.owner = get(User, username="owner")
self.member = get(User, username="member")
self.project = get(Project, slug="project")
self.client.force_login(self.owner)

def test_unspecified_slug_redirects_to_organization_edit(self):
self.assertEqual(Organization.objects.count(), 0)
resp = self.client.get(reverse("organization_members", kwargs={"slug": "-"}))
self.assertRedirects(
resp,
reverse(
"organization_choose", kwargs={"next_name": "organization_members"}
),
)

def test_choose_organization_edit(self):
self.assertEqual(Organization.objects.count(), 0)
resp = self.client.get(
reverse("organization_choose", kwargs={"next_name": "organization_edit"})
)
self.assertContains(resp, "You aren't currently a member of any organizations")
5 changes: 5 additions & 0 deletions readthedocs/organizations/urls/private.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
views.ListOrganization.as_view(),
name='organization_list',
),
re_path(
r"^choose/(?P<next_name>[\w.-]+)/$",
views.ChooseOrganization.as_view(),
name="organization_choose",
),
re_path(
r'^create/$',
views.CreateOrganizationSignup.as_view(),
Expand Down
43 changes: 20 additions & 23 deletions readthedocs/organizations/views/base.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
"""Base classes for organization views."""
from functools import lru_cache

from django.conf import settings
from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy

Expand All @@ -20,27 +18,14 @@
Team,
TeamMember,
)


class CheckOrganizationsEnabled:

"""
Return 404 if organizations aren't enabled.

All organization views should inherit this class.
This is mainly for our tests to work,
adding the organization urls conditionally on readthedocs/urls.py
doesn't work as the file is evaluated only once, not per-test case.
"""

def dispatch(self, *args, **kwargs):
if not settings.RTD_ALLOW_ORGANIZATIONS:
raise Http404
return super().dispatch(*args, **kwargs)
from readthedocs.organizations.views.decorators import (
redirect_if_organization_unspecified,
redirect_if_organizations_disabled,
)


# Mixins
class OrganizationMixin(CheckOrganizationsEnabled):
class OrganizationMixin:

"""
Mixin class that provides organization sublevel objects.
Expand All @@ -58,6 +43,11 @@ class OrganizationMixin(CheckOrganizationsEnabled):
org_url_field = 'slug'
admin_only = True

@redirect_if_organizations_disabled
@redirect_if_organization_unspecified
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)

def get_queryset(self):
"""Return queryset that returns organizations for user."""
return self.get_organization_queryset()
Expand Down Expand Up @@ -128,16 +118,23 @@ def get_form(self, data=None, files=None, **kwargs):


# Base views
class OrganizationView(CheckOrganizationsEnabled):
class OrganizationView:

"""Mixin for an organization view that doesn't have nested components."""

model = Organization
lookup_field = 'slug'
lookup_url_field = 'slug'
form_class = OrganizationForm
admin_only = True

# Only relevant when mixed into
lookup_field = 'slug'
lookup_url_field = 'slug'

@redirect_if_organizations_disabled
@redirect_if_organization_unspecified
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)

def get_queryset(self):
if self.admin_only:
return Organization.objects.for_admin_user(user=self.request.user)
Expand Down
Loading