New subproject admin page (#2957)
* Remove privacy app The privacy app was a strange mixture of various application models managers, querysets, and syncers. Instead, logic is moved to where we should be using it, inside the other applications * New subproject admin page This replaces the list of text with a table that is more navigable and also replaces the create form with a dropdown form of the projects you can add. This list of projects is limited to projects you are the admin of, and which have not been added as a sub or super project anywhere else. This depends on #2954 and others. * Lint fixes * Require arguments to project formproject-feature-flip-apiv2
parent
a828543dce
commit
2d4e004bee
|
@ -1102,6 +1102,12 @@ div.httpexchange div.highlight pre {
|
|||
font-size: .9em;
|
||||
}
|
||||
|
||||
/* Subprojects */
|
||||
div.module.project-subprojects div.subproject-meta {
|
||||
font-size: .9em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Pygments */
|
||||
div.highlight pre .hll { background-color: #ffffcc }
|
||||
div.highlight pre .c { color: #60a0b0; font-style: italic } /* Comment */
|
||||
|
|
|
@ -21,7 +21,8 @@ from readthedocs.integrations.models import Integration
|
|||
from readthedocs.oauth.models import RemoteRepository
|
||||
from readthedocs.projects import constants
|
||||
from readthedocs.projects.exceptions import ProjectSpamError
|
||||
from readthedocs.projects.models import Project, EmailHook, WebHook, Domain
|
||||
from readthedocs.projects.models import (
|
||||
Project, ProjectRelationship, EmailHook, WebHook, Domain)
|
||||
from readthedocs.redirects.models import Redirect
|
||||
|
||||
from future import standard_library
|
||||
|
@ -236,6 +237,46 @@ class UpdateProjectForm(ProjectTriggerBuildMixin, ProjectBasicsForm,
|
|||
)
|
||||
|
||||
|
||||
class ProjectRelationshipForm(forms.ModelForm):
|
||||
|
||||
"""Form to add/update project relationships"""
|
||||
|
||||
parent = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
|
||||
class Meta(object):
|
||||
model = ProjectRelationship
|
||||
exclude = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.project = kwargs.pop('project')
|
||||
self.user = kwargs.pop('user')
|
||||
super(ProjectRelationshipForm, self).__init__(*args, **kwargs)
|
||||
# Don't display the update form with an editable child, as it will be
|
||||
# filtered out from the queryset anyways.
|
||||
if hasattr(self, 'instance') and self.instance.pk is not None:
|
||||
self.fields['child'].disabled = True
|
||||
else:
|
||||
self.fields['child'].queryset = self.get_subproject_queryset()
|
||||
|
||||
def clean_parent(self):
|
||||
if self.project.superprojects.exists():
|
||||
# This validation error is mostly for testing, users shouldn't see
|
||||
# this in normal circumstances
|
||||
raise forms.ValidationError(_("Subproject nesting is not supported"))
|
||||
return self.project
|
||||
|
||||
def get_subproject_queryset(self):
|
||||
"""Return scrubbed subproject choice queryset
|
||||
|
||||
This removes projects that are either already a subproject of another
|
||||
project, or are a superproject, as neither case is supported.
|
||||
"""
|
||||
queryset = (Project.objects.for_admin_user(self.user)
|
||||
.exclude(subprojects__isnull=False)
|
||||
.exclude(superprojects__isnull=False))
|
||||
return queryset
|
||||
|
||||
|
||||
class DualCheckboxWidget(forms.CheckboxInput):
|
||||
|
||||
"""Checkbox with link to the version's built documentation"""
|
||||
|
@ -364,44 +405,6 @@ def build_upload_html_form(project):
|
|||
return type('UploadHTMLForm', (BaseUploadHTMLForm,), attrs)
|
||||
|
||||
|
||||
class SubprojectForm(forms.Form):
|
||||
|
||||
"""Project subproject form"""
|
||||
|
||||
subproject = forms.CharField()
|
||||
alias = forms.CharField(required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.user = kwargs.pop('user')
|
||||
self.parent = kwargs.pop('parent')
|
||||
super(SubprojectForm, self).__init__(*args, **kwargs)
|
||||
|
||||
def clean_subproject(self):
|
||||
"""Normalize subproject field
|
||||
|
||||
Does lookup on against :py:class:`Project` to ensure matching project
|
||||
exists. Return the :py:class:`Project` object instead.
|
||||
"""
|
||||
subproject_name = self.cleaned_data['subproject']
|
||||
subproject_qs = Project.objects.filter(slug=subproject_name)
|
||||
if not subproject_qs.exists():
|
||||
raise forms.ValidationError((_("Project %(name)s does not exist")
|
||||
% {'name': subproject_name}))
|
||||
subproject = subproject_qs.first()
|
||||
if not AdminPermission.is_admin(self.user, subproject):
|
||||
raise forms.ValidationError(_(
|
||||
'You need to be admin of {name} in order to add it as '
|
||||
'a subproject.'.format(name=subproject_name)))
|
||||
return subproject
|
||||
|
||||
def save(self):
|
||||
relationship = self.parent.add_subproject(
|
||||
self.cleaned_data['subproject'],
|
||||
alias=self.cleaned_data['alias'],
|
||||
)
|
||||
return relationship
|
||||
|
||||
|
||||
class UserForm(forms.Form):
|
||||
|
||||
"""Project user association form"""
|
||||
|
|
|
@ -64,14 +64,6 @@ urlpatterns = [
|
|||
private.project_delete,
|
||||
name='projects_delete'),
|
||||
|
||||
url(r'^(?P<project_slug>[-\w]+)/subprojects/delete/(?P<child_slug>[-\w]+)/$', # noqa
|
||||
private.project_subprojects_delete,
|
||||
name='projects_subprojects_delete'),
|
||||
|
||||
url(r'^(?P<project_slug>[-\w]+)/subprojects/$',
|
||||
private.project_subprojects,
|
||||
name='projects_subprojects'),
|
||||
|
||||
url(r'^(?P<project_slug>[-\w]+)/users/$',
|
||||
private.project_users,
|
||||
name='projects_users'),
|
||||
|
@ -165,3 +157,25 @@ integration_urls = [
|
|||
]
|
||||
|
||||
urlpatterns += integration_urls
|
||||
|
||||
subproject_urls = [
|
||||
url(r'^(?P<project_slug>{project_slug})/subprojects/$'.format(**pattern_opts),
|
||||
private.ProjectRelationshipList.as_view(),
|
||||
name='projects_subprojects'),
|
||||
url((r'^(?P<project_slug>{project_slug})/subprojects/create/$'
|
||||
.format(**pattern_opts)),
|
||||
private.ProjectRelationshipCreate.as_view(),
|
||||
name='projects_subprojects_create'),
|
||||
url((r'^(?P<project_slug>{project_slug})/'
|
||||
r'subprojects/(?P<subproject_slug>{project_slug})/edit/$'
|
||||
.format(**pattern_opts)),
|
||||
private.ProjectRelationshipUpdate.as_view(),
|
||||
name='projects_subprojects_update'),
|
||||
url((r'^(?P<project_slug>{project_slug})/'
|
||||
r'subprojects/(?P<subproject_slug>{project_slug})/delete/$'
|
||||
.format(**pattern_opts)),
|
||||
private.ProjectRelationshipDelete.as_view(),
|
||||
name='projects_subprojects_delete'),
|
||||
]
|
||||
|
||||
urlpatterns += subproject_urls
|
||||
|
|
|
@ -28,12 +28,13 @@ from readthedocs.core.utils import trigger_build, broadcast
|
|||
from readthedocs.core.mixins import ListViewWithForm
|
||||
from readthedocs.integrations.models import HttpExchange, Integration
|
||||
from readthedocs.projects.forms import (
|
||||
ProjectBasicsForm, ProjectExtraForm,
|
||||
ProjectAdvancedForm, UpdateProjectForm, SubprojectForm,
|
||||
ProjectBasicsForm, ProjectExtraForm, ProjectAdvancedForm,
|
||||
UpdateProjectForm, ProjectRelationshipForm,
|
||||
build_versions_form, UserForm, EmailHookForm, TranslationForm,
|
||||
RedirectForm, WebHookForm, DomainForm, IntegrationForm,
|
||||
ProjectAdvertisingForm)
|
||||
from readthedocs.projects.models import Project, EmailHook, WebHook, Domain
|
||||
from readthedocs.projects.models import (
|
||||
Project, ProjectRelationship, EmailHook, WebHook, Domain)
|
||||
from readthedocs.projects.views.base import ProjectAdminMixin, ProjectSpamMixin
|
||||
from readthedocs.projects import tasks
|
||||
from readthedocs.oauth.services import registry
|
||||
|
@ -398,7 +399,7 @@ def edit_alias(request, project_slug, alias_id=None):
|
|||
class AliasList(PrivateViewMixin, ListView):
|
||||
model = VersionAlias
|
||||
template_context_name = 'alias'
|
||||
template_name = 'projects/alias_list.html',
|
||||
template_name = 'projects/alias_list.html'
|
||||
|
||||
def get_queryset(self):
|
||||
self.project = get_object_or_404(
|
||||
|
@ -407,44 +408,50 @@ class AliasList(PrivateViewMixin, ListView):
|
|||
return self.project.aliases.all()
|
||||
|
||||
|
||||
@login_required
|
||||
def project_subprojects(request, project_slug):
|
||||
"""Project subprojects view and form view"""
|
||||
project = get_object_or_404(Project.objects.for_admin_user(request.user),
|
||||
slug=project_slug)
|
||||
class ProjectRelationshipMixin(ProjectAdminMixin, PrivateViewMixin):
|
||||
|
||||
form_kwargs = {
|
||||
'parent': project,
|
||||
'user': request.user,
|
||||
}
|
||||
if request.method == 'POST':
|
||||
form = SubprojectForm(request.POST, **form_kwargs)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
broadcast(type='app', task=tasks.symlink_subproject, args=[project.pk])
|
||||
project_dashboard = reverse(
|
||||
'projects_subprojects', args=[project.slug])
|
||||
return HttpResponseRedirect(project_dashboard)
|
||||
else:
|
||||
form = SubprojectForm(**form_kwargs)
|
||||
model = ProjectRelationship
|
||||
form_class = ProjectRelationshipForm
|
||||
lookup_field = 'child__slug'
|
||||
lookup_url_kwarg = 'subproject_slug'
|
||||
|
||||
subprojects = project.subprojects.all()
|
||||
def get_queryset(self):
|
||||
self.project = self.get_project()
|
||||
return self.model.objects.filter(parent=self.project)
|
||||
|
||||
return render_to_response(
|
||||
'projects/project_subprojects.html',
|
||||
{'form': form, 'project': project, 'subprojects': subprojects},
|
||||
context_instance=RequestContext(request)
|
||||
)
|
||||
def get_form(self, data=None, files=None, **kwargs):
|
||||
kwargs['user'] = self.request.user
|
||||
return super(ProjectRelationshipMixin, self).get_form(data, files, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
broadcast(type='app', task=tasks.symlink_subproject,
|
||||
args=[self.get_project().pk])
|
||||
return super(ProjectRelationshipMixin, self).form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('projects_subprojects', args=[self.get_project().slug])
|
||||
|
||||
|
||||
@login_required
|
||||
def project_subprojects_delete(request, project_slug, child_slug):
|
||||
parent = get_object_or_404(Project.objects.for_admin_user(request.user), slug=project_slug)
|
||||
child = get_object_or_404(Project.objects.all(), slug=child_slug)
|
||||
parent.remove_subproject(child)
|
||||
broadcast(type='app', task=tasks.symlink_subproject, args=[parent.pk])
|
||||
return HttpResponseRedirect(reverse('projects_subprojects',
|
||||
args=[parent.slug]))
|
||||
class ProjectRelationshipList(ProjectRelationshipMixin, ListView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super(ProjectRelationshipList, self).get_context_data(**kwargs)
|
||||
ctx['superproject'] = self.project.superprojects.first()
|
||||
return ctx
|
||||
|
||||
|
||||
class ProjectRelationshipCreate(ProjectRelationshipMixin, CreateView):
|
||||
pass
|
||||
|
||||
|
||||
class ProjectRelationshipUpdate(ProjectRelationshipMixin, UpdateView):
|
||||
pass
|
||||
|
||||
|
||||
class ProjectRelationshipDelete(ProjectRelationshipMixin, DeleteView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return self.http_method_not_allowed(request, *args, **kwargs)
|
||||
|
||||
|
||||
@login_required
|
||||
|
|
|
@ -142,6 +142,7 @@ class ProjectMixin(URLAccessMixin):
|
|||
self.domain = get(Domain, url='http://docs.foobar.com', project=self.pip)
|
||||
self.default_kwargs = {
|
||||
'project_slug': self.pip.slug,
|
||||
'subproject_slug': self.subproject.slug,
|
||||
'version_slug': self.pip.versions.all()[0].slug,
|
||||
'filename': 'index.html',
|
||||
'type_': 'pdf',
|
||||
|
@ -227,6 +228,7 @@ class PrivateProjectAdminAccessTest(PrivateProjectMixin, TestCase):
|
|||
'/dashboard/pip/users/delete/': {'status_code': 405},
|
||||
'/dashboard/pip/notifications/delete/': {'status_code': 405},
|
||||
'/dashboard/pip/redirects/delete/': {'status_code': 405},
|
||||
'/dashboard/pip/subprojects/sub/delete/': {'status_code': 405},
|
||||
'/dashboard/pip/integrations/sync/': {'status_code': 405},
|
||||
'/dashboard/pip/integrations/1/sync/': {'status_code': 405},
|
||||
'/dashboard/pip/integrations/1/delete/': {'status_code': 405},
|
||||
|
@ -255,6 +257,7 @@ class PrivateProjectUserAccessTest(PrivateProjectMixin, TestCase):
|
|||
'/dashboard/pip/users/delete/': {'status_code': 405},
|
||||
'/dashboard/pip/notifications/delete/': {'status_code': 405},
|
||||
'/dashboard/pip/redirects/delete/': {'status_code': 405},
|
||||
'/dashboard/pip/subprojects/sub/delete/': {'status_code': 405},
|
||||
'/dashboard/pip/integrations/sync/': {'status_code': 405},
|
||||
'/dashboard/pip/integrations/1/sync/': {'status_code': 405},
|
||||
'/dashboard/pip/integrations/1/delete/': {'status_code': 405},
|
||||
|
|
|
@ -381,6 +381,21 @@ class TestPrivateViews(MockBuildTestCase):
|
|||
task=tasks.remove_dir,
|
||||
args=[project.doc_path])
|
||||
|
||||
def test_subproject_create(self):
|
||||
project = get(Project, slug='pip', users=[self.user])
|
||||
subproject = get(Project, users=[self.user])
|
||||
|
||||
with patch('readthedocs.projects.views.private.broadcast') as broadcast:
|
||||
response = self.client.post(
|
||||
'/dashboard/pip/subprojects/create/',
|
||||
data={'child': subproject.pk},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
broadcast.assert_called_with(
|
||||
type='app',
|
||||
task=tasks.symlink_subproject,
|
||||
args=[project.pk])
|
||||
|
||||
|
||||
class TestPrivateMixins(MockBuildTestCase):
|
||||
|
||||
|
|
|
@ -1,66 +1,147 @@
|
|||
from __future__ import absolute_import
|
||||
import mock
|
||||
|
||||
import mock
|
||||
import django_dynamic_fixture as fixture
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from readthedocs.projects.forms import SubprojectForm
|
||||
from readthedocs.projects.models import Project
|
||||
from readthedocs.projects.forms import ProjectRelationshipForm
|
||||
from readthedocs.projects.models import Project, ProjectRelationship
|
||||
from readthedocs.rtd_tests.utils import create_user
|
||||
|
||||
from django_dynamic_fixture import get
|
||||
|
||||
|
||||
class SubprojectFormTests(TestCase):
|
||||
|
||||
def test_name_validation(self):
|
||||
user = get(User)
|
||||
project = get(Project, slug='mainproject')
|
||||
|
||||
form = SubprojectForm({},
|
||||
parent=project, user=user)
|
||||
def test_empty_child(self):
|
||||
user = fixture.get(User)
|
||||
project = fixture.get(Project, slug='mainproject')
|
||||
form = ProjectRelationshipForm(
|
||||
{},
|
||||
project=project,
|
||||
user=user
|
||||
)
|
||||
form.full_clean()
|
||||
self.assertTrue('subproject' in form.errors)
|
||||
self.assertEqual(len(form.errors['child']), 1)
|
||||
self.assertRegexpMatches(
|
||||
form.errors['child'][0],
|
||||
r'This field is required.'
|
||||
)
|
||||
|
||||
form = SubprojectForm({'name': 'not-existent'},
|
||||
parent=project, user=user)
|
||||
def test_nonexistent_child(self):
|
||||
user = fixture.get(User)
|
||||
project = fixture.get(Project, slug='mainproject')
|
||||
self.assertFalse(Project.objects.filter(pk=9999).exists())
|
||||
form = ProjectRelationshipForm(
|
||||
{'child': 9999},
|
||||
project=project,
|
||||
user=user
|
||||
)
|
||||
form.full_clean()
|
||||
self.assertTrue('subproject' in form.errors)
|
||||
self.assertEqual(len(form.errors['child']), 1)
|
||||
self.assertRegexpMatches(
|
||||
form.errors['child'][0],
|
||||
r'Select a valid choice.'
|
||||
)
|
||||
|
||||
def test_adding_subproject_fails_when_user_is_not_admin(self):
|
||||
# Make sure that a user cannot add a subproject that he is not the
|
||||
# admin of.
|
||||
|
||||
user = get(User)
|
||||
project = get(Project, slug='mainproject')
|
||||
user = fixture.get(User)
|
||||
project = fixture.get(Project, slug='mainproject')
|
||||
project.users.add(user)
|
||||
subproject = get(Project, slug='subproject')
|
||||
|
||||
form = SubprojectForm({'subproject': subproject.slug},
|
||||
parent=project, user=user)
|
||||
# Fails because user does not own subproject.
|
||||
subproject = fixture.get(Project, slug='subproject')
|
||||
self.assertQuerysetEqual(
|
||||
Project.objects.for_admin_user(user),
|
||||
[project],
|
||||
transform=lambda n: n,
|
||||
)
|
||||
form = ProjectRelationshipForm(
|
||||
{'child': subproject.pk},
|
||||
project=project,
|
||||
user=user
|
||||
)
|
||||
form.full_clean()
|
||||
self.assertTrue('subproject' in form.errors)
|
||||
self.assertEqual(len(form.errors['child']), 1)
|
||||
self.assertRegexpMatches(
|
||||
form.errors['child'][0],
|
||||
r'Select a valid choice.'
|
||||
)
|
||||
|
||||
def test_admin_of_subproject_can_add_it(self):
|
||||
user = get(User)
|
||||
project = get(Project, slug='mainproject')
|
||||
def test_adding_subproject_passes_when_user_is_admin(self):
|
||||
user = fixture.get(User)
|
||||
project = fixture.get(Project, slug='mainproject')
|
||||
project.users.add(user)
|
||||
subproject = get(Project, slug='subproject')
|
||||
subproject = fixture.get(Project, slug='subproject')
|
||||
subproject.users.add(user)
|
||||
|
||||
# Works now as user is admin of subproject.
|
||||
form = SubprojectForm({'subproject': subproject.slug},
|
||||
parent=project, user=user)
|
||||
# Fails because user does not own subproject.
|
||||
self.assertQuerysetEqual(
|
||||
Project.objects.for_admin_user(user),
|
||||
[project, subproject],
|
||||
transform=lambda n: n,
|
||||
)
|
||||
form = ProjectRelationshipForm(
|
||||
{'child': subproject.pk},
|
||||
project=project,
|
||||
user=user
|
||||
)
|
||||
form.full_clean()
|
||||
self.assertTrue(form.is_valid())
|
||||
form.save()
|
||||
|
||||
self.assertEqual(
|
||||
[r.child for r in project.subprojects.all()],
|
||||
[subproject])
|
||||
[subproject]
|
||||
)
|
||||
|
||||
def test_subproject_form_cant_create_sub_sub_project(self):
|
||||
user = fixture.get(User)
|
||||
project = fixture.get(Project, users=[user])
|
||||
subproject = fixture.get(Project, users=[user])
|
||||
subsubproject = fixture.get(Project, users=[user])
|
||||
relation = fixture.get(
|
||||
ProjectRelationship, parent=project, child=subproject
|
||||
)
|
||||
self.assertQuerysetEqual(
|
||||
Project.objects.for_admin_user(user),
|
||||
[project, subproject, subsubproject],
|
||||
transform=lambda n: n,
|
||||
)
|
||||
form = ProjectRelationshipForm(
|
||||
{'child': subsubproject.pk},
|
||||
project=subproject,
|
||||
user=user
|
||||
)
|
||||
# The subsubproject is valid here, as far as the child check is
|
||||
# concerned, but the parent check should fail.
|
||||
self.assertEqual(
|
||||
[proj_id for (proj_id, __) in form.fields['child'].choices],
|
||||
['', subsubproject.pk],
|
||||
)
|
||||
form.full_clean()
|
||||
self.assertEqual(len(form.errors['parent']), 1)
|
||||
self.assertRegexpMatches(
|
||||
form.errors['parent'][0],
|
||||
r'Subproject nesting is not supported'
|
||||
)
|
||||
|
||||
def test_excludes_existing_subprojects(self):
|
||||
user = fixture.get(User)
|
||||
project = fixture.get(Project, users=[user])
|
||||
subproject = fixture.get(Project, users=[user])
|
||||
relation = fixture.get(
|
||||
ProjectRelationship, parent=project, child=subproject
|
||||
)
|
||||
self.assertQuerysetEqual(
|
||||
Project.objects.for_admin_user(user),
|
||||
[project, subproject],
|
||||
transform=lambda n: n,
|
||||
)
|
||||
form = ProjectRelationshipForm(
|
||||
{'child': subproject.pk},
|
||||
project=project,
|
||||
user=user
|
||||
)
|
||||
self.assertEqual(
|
||||
[proj_id for (proj_id, __) in form.fields['child'].choices],
|
||||
[''],
|
||||
)
|
||||
|
||||
|
||||
@override_settings(PUBLIC_DOMAIN='readthedocs.org')
|
||||
|
@ -70,11 +151,13 @@ class ResolverBase(TestCase):
|
|||
with mock.patch('readthedocs.projects.models.broadcast'):
|
||||
self.owner = create_user(username='owner', password='test')
|
||||
self.tester = create_user(username='tester', password='test')
|
||||
self.pip = get(Project, slug='pip', users=[self.owner], main_language_project=None)
|
||||
self.subproject = get(Project, slug='sub', language='ja', users=[
|
||||
self.owner], main_language_project=None)
|
||||
self.translation = get(Project, slug='trans', language='ja', users=[
|
||||
self.owner], main_language_project=None)
|
||||
self.pip = fixture.get(Project, slug='pip', users=[self.owner], main_language_project=None)
|
||||
self.subproject = fixture.get(Project, slug='sub', language='ja',
|
||||
users=[ self.owner],
|
||||
main_language_project=None)
|
||||
self.translation = fixture.get(Project, slug='trans', language='ja',
|
||||
users=[ self.owner],
|
||||
main_language_project=None)
|
||||
self.pip.add_subproject(self.subproject)
|
||||
self.pip.translations.add(self.translation)
|
||||
|
||||
|
|
|
@ -113,8 +113,13 @@ class PrivateViewsAreProtectedTests(TestCase):
|
|||
self.assertRedirectToLogin(response)
|
||||
|
||||
def test_subprojects_delete(self):
|
||||
# This URL doesn't exist anymore, 404
|
||||
response = self.client.get(
|
||||
'/dashboard/pip/subprojects/delete/a-subproject/')
|
||||
self.assertEqual(response.status_code, 404)
|
||||
# New URL
|
||||
response = self.client.get(
|
||||
'/dashboard/pip/subprojects/a-subproject/delete/')
|
||||
self.assertRedirectToLogin(response)
|
||||
|
||||
def test_subprojects(self):
|
||||
|
@ -213,7 +218,18 @@ class SubprojectViewTests(TestCase):
|
|||
self.project.users.add(self.user)
|
||||
self.subproject.users.add(self.user)
|
||||
|
||||
response = self.client.get('/dashboard/my-mainproject/subprojects/delete/my-subproject/')
|
||||
# URL doesn't exist anymore, 404
|
||||
response = self.client.get(
|
||||
'/dashboard/my-mainproject/subprojects/delete/my-subproject/')
|
||||
self.assertEqual(response.status_code, 404)
|
||||
# This URL still doesn't accept GET, 405
|
||||
response = self.client.get(
|
||||
'/dashboard/my-mainproject/subprojects/my-subproject/delete/')
|
||||
self.assertEqual(response.status_code, 405)
|
||||
self.assertTrue(self.subproject in [r.child for r in self.project.subprojects.all()])
|
||||
# Test POST
|
||||
response = self.client.post(
|
||||
'/dashboard/my-mainproject/subprojects/my-subproject/delete/')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue(self.subproject not in [r.child for r in self.project.subprojects.all()])
|
||||
|
||||
|
@ -221,6 +237,7 @@ class SubprojectViewTests(TestCase):
|
|||
self.project.users.add(self.user)
|
||||
self.assertFalse(AdminPermission.is_admin(self.user, self.subproject))
|
||||
|
||||
response = self.client.get('/dashboard/my-mainproject/subprojects/delete/my-subproject/')
|
||||
response = self.client.post(
|
||||
'/dashboard/my-mainproject/subprojects/my-subproject/delete/')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue(self.subproject not in [r.child for r in self.project.subprojects.all()])
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
{% extends "projects/project_edit_base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Edit Subprojects" %}{% endblock %}
|
||||
|
||||
{% block nav-dashboard %} class="active"{% endblock %}
|
||||
|
||||
{% block editing-option-edit-proj %}class="active"{% endblock %}
|
||||
|
||||
{% block project-subprojects-active %}active{% endblock %}
|
||||
{% block project_edit_content_header %}{% trans "Subprojects" %}{% endblock %}
|
||||
|
||||
{% block project_edit_content %}
|
||||
<p class="help_text">
|
||||
{% trans "This allows you to add subprojects to your project. This allows them to live in the same namespace in the URLConf for a subdomain or CNAME." %}
|
||||
</p>
|
||||
|
||||
<h3> {% trans "Existing Subprojects" %} </h3>
|
||||
<p>
|
||||
<ul>
|
||||
{% for relationship in subprojects %}
|
||||
<li>
|
||||
<a href="{{ relationship.get_absolute_url }}">
|
||||
{{ relationship.child }}
|
||||
</a>
|
||||
(/projects/{% if relationship.alias %}{{ relationship.alias }}{% else %}{{ relationship.child.slug }}{%endif %}/)
|
||||
(<a href="{% url "projects_subprojects_delete" relationship.parent.slug relationship.child.slug %}">{% trans "Remove" %}</a>)
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p>
|
||||
{% trans "Choose which project you would like to add as a subproject." %}
|
||||
</p>
|
||||
<form method="post" action=".">{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<p>
|
||||
<input style="display: inline;" type="submit" value="{% trans "Submit" %}">
|
||||
</p>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block footerjs %}
|
||||
$('#id_subproject').autocomplete({
|
||||
source: '{% url "search_autocomplete" %}',
|
||||
minLength: 2,
|
||||
open: function(event, ui) {
|
||||
ac_top = $('.ui-autocomplete').css('top');
|
||||
$('.ui-autocomplete').css({'width': '233px', 'top': ac_top + 10 });
|
||||
}
|
||||
});
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,41 @@
|
|||
{% extends "projects/project_edit_base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Subprojects" %}{% endblock %}
|
||||
|
||||
{% block nav-dashboard %} class="active"{% endblock %}
|
||||
|
||||
{% block editing-option-edit-proj %}class="active"{% endblock %}
|
||||
|
||||
{% block project-subprojects-active %}active{% endblock %}
|
||||
{% block project_edit_content_header %}{% trans "Subprojects" %}{% endblock %}
|
||||
|
||||
{% block project_edit_content %}
|
||||
{% if object %}
|
||||
<form
|
||||
method="post"
|
||||
class="form-wide"
|
||||
action="{% url 'projects_subprojects_update' project_slug=project.slug subproject_slug=object.child.slug %}">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="{% trans "Update subproject" %}">
|
||||
</form>
|
||||
|
||||
<form
|
||||
method="post"
|
||||
action="{% url 'projects_subprojects_delete' project_slug=project.slug subproject_slug=object.child.slug %}">
|
||||
{% csrf_token %}
|
||||
<input type="submit" value="{% trans "Delete subproject" %}">
|
||||
</form>
|
||||
{% else %}
|
||||
<form
|
||||
method="post"
|
||||
class="form-wide"
|
||||
action="{% url 'projects_subprojects_create' project_slug=project.slug %}">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="{% trans "Add subproject" %}">
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -0,0 +1,78 @@
|
|||
{% extends "projects/project_edit_base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Subprojects" %}{% endblock %}
|
||||
|
||||
{% block nav-dashboard %} class="active"{% endblock %}
|
||||
|
||||
{% block editing-option-edit-proj %}class="active"{% endblock %}
|
||||
|
||||
{% block project-subprojects-active %}active{% endblock %}
|
||||
{% block project_edit_content_header %}{% trans "Subprojects" %}{% endblock %}
|
||||
|
||||
{% block project_edit_content %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Subprojects are projects hosted from the same URL as their parent project.
|
||||
This is useful for organizing multiple projects under a custom domain.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
{% if superproject %}
|
||||
<p>
|
||||
{% blocktrans trimmed with project=superproject.parent.name %}
|
||||
This project is already configured as a subproject of {{ project }}.
|
||||
Nested subprojects are not currently supported.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<a href="{% url 'projects_subprojects' project_slug=superproject.parent.slug %}">
|
||||
{% blocktrans trimmed with project=superproject.parent.name %}
|
||||
View subprojects of {{ project }}
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="button-bar">
|
||||
<ul>
|
||||
<li>
|
||||
<a class="button"
|
||||
href="{% url 'projects_subprojects_create' project_slug=project.slug %}">
|
||||
{% trans "Add subproject" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="module project-subprojects">
|
||||
<div class="module-list-wrapper">
|
||||
<ul>
|
||||
{% for subproject in object_list %}
|
||||
<li class="module-item">
|
||||
<div class="subproject-name">
|
||||
<a href="{% url 'projects_subprojects_update' project_slug=project.slug subproject_slug=subproject.child.slug %}">
|
||||
{{ subproject.child }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="subproject-meta">
|
||||
<a href="{% url 'projects_manage' project_slug=subproject.child.slug %}">
|
||||
{% trans "Project page" %}</a>
|
||||
‐
|
||||
<a href="{{ subproject.get_absolute_url }}">
|
||||
{{ subproject.get_absolute_url }}
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
{% empty %}
|
||||
<li class="module-item">
|
||||
<p class="quiet">
|
||||
{% trans 'No subprojects are currently configured' %}
|
||||
</p>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
Loading…
Reference in New Issue