Project updated when subproject modified (#3649)
* Test for changing subproject privacy level * Proper test for privacy level on subprojects * Trigger re-symlink for superproject when project changes Re-symlink when: * a subproject is deleted * a subproject privacy level is changed * a subproject version privacy level is changed * Update test case for current implementation * Revert "Trigger re-symlink for superproject when project changes" This reverts commit 3fe6cb3f3dfddc87d8c1e658cb7f3ebad4f6f476. * Move logic from Form to Model Instead of trigger the re-symlink task on each of the Form actions, we trigger it once on ``Project.save()`` or ``Project.delete()`` method. * Test for calls to broadcast utility on Project.save()humitos/django/compatibility
parent
1edd47a0cd
commit
cc18a75de8
|
@ -326,8 +326,26 @@ class Project(models.Model):
|
||||||
log.exception('failed to sync supported versions')
|
log.exception('failed to sync supported versions')
|
||||||
try:
|
try:
|
||||||
if not first_save:
|
if not first_save:
|
||||||
broadcast(type='app', task=tasks.symlink_project,
|
log.info(
|
||||||
args=[self.pk],)
|
'Re-symlinking project and subprojects: project=%s',
|
||||||
|
self.slug,
|
||||||
|
)
|
||||||
|
broadcast(
|
||||||
|
type='app',
|
||||||
|
task=tasks.symlink_project,
|
||||||
|
args=[self.pk],
|
||||||
|
)
|
||||||
|
log.info(
|
||||||
|
'Re-symlinking superprojects: project=%s',
|
||||||
|
self.slug,
|
||||||
|
)
|
||||||
|
for superproject in self.superprojects.all():
|
||||||
|
broadcast(
|
||||||
|
type='app',
|
||||||
|
task=tasks.symlink_project,
|
||||||
|
args=[superproject.pk],
|
||||||
|
)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
log.exception('failed to symlink project')
|
log.exception('failed to symlink project')
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -5,16 +5,16 @@ from builtins import object
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
import collections
|
|
||||||
from functools import wraps
|
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
from django_dynamic_fixture import get
|
from django_dynamic_fixture import get
|
||||||
|
|
||||||
from readthedocs.builds.models import Version
|
from readthedocs.builds.models import Version
|
||||||
from readthedocs.projects.models import Project, Domain
|
from readthedocs.projects.models import Project, Domain
|
||||||
|
from readthedocs.projects.tasks import symlink_project
|
||||||
from readthedocs.core.symlink import PublicSymlink, PrivateSymlink
|
from readthedocs.core.symlink import PublicSymlink, PrivateSymlink
|
||||||
|
|
||||||
|
|
||||||
|
@ -908,3 +908,202 @@ class TestPublicSymlinkUnicode(TempSiterootCase, TestCase):
|
||||||
self.symlink.run()
|
self.symlink.run()
|
||||||
except:
|
except:
|
||||||
self.fail('Symlink run raised an exception on unicode slug')
|
self.fail('Symlink run raised an exception on unicode slug')
|
||||||
|
|
||||||
|
def test_symlink_broadcast_calls_on_project_save(self):
|
||||||
|
"""
|
||||||
|
Test calls to ``readthedocs.core.utils.broadcast`` on Project.save().
|
||||||
|
|
||||||
|
When a Project is saved, we need to check that we are calling
|
||||||
|
``broadcast`` utility with the proper task and arguments to re-symlink
|
||||||
|
them.
|
||||||
|
"""
|
||||||
|
with mock.patch('readthedocs.projects.models.broadcast') as broadcast:
|
||||||
|
project = get(Project)
|
||||||
|
# skipped on first save
|
||||||
|
broadcast.assert_not_called()
|
||||||
|
|
||||||
|
broadcast.reset_mock()
|
||||||
|
project.description = 'New description'
|
||||||
|
project.save()
|
||||||
|
# called once for this project itself
|
||||||
|
broadcast.assert_any_calls(
|
||||||
|
type='app',
|
||||||
|
task=symlink_project,
|
||||||
|
args=[project.pk],
|
||||||
|
)
|
||||||
|
|
||||||
|
broadcast.reset_mock()
|
||||||
|
subproject = get(Project)
|
||||||
|
# skipped on first save
|
||||||
|
broadcast.assert_not_called()
|
||||||
|
|
||||||
|
project.add_subproject(subproject)
|
||||||
|
# subproject.save() is not called
|
||||||
|
broadcast.assert_not_called()
|
||||||
|
|
||||||
|
subproject.description = 'New subproject description'
|
||||||
|
subproject.save()
|
||||||
|
# subproject symlinks
|
||||||
|
broadcast.assert_any_calls(
|
||||||
|
type='app',
|
||||||
|
task=symlink_project,
|
||||||
|
args=[subproject.pk],
|
||||||
|
)
|
||||||
|
# superproject symlinks
|
||||||
|
broadcast.assert_any_calls(
|
||||||
|
type='app',
|
||||||
|
task=symlink_project,
|
||||||
|
args=[project.pk],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings()
|
||||||
|
class TestPublicPrivateSymlink(TempSiterootCase, TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestPublicPrivateSymlink, self).setUp()
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
self.user = get(User)
|
||||||
|
self.project = get(
|
||||||
|
Project, name='project', slug='project', privacy_level='public',
|
||||||
|
users=[self.user], main_language_project=None)
|
||||||
|
self.project.versions.update(privacy_level='public')
|
||||||
|
self.project.save()
|
||||||
|
|
||||||
|
self.subproject = get(
|
||||||
|
Project, name='subproject', slug='subproject', privacy_level='public',
|
||||||
|
users=[self.user], main_language_project=None)
|
||||||
|
self.subproject.versions.update(privacy_level='public')
|
||||||
|
self.subproject.save()
|
||||||
|
|
||||||
|
def test_change_subproject_privacy(self):
|
||||||
|
"""
|
||||||
|
Change subproject's ``privacy_level`` creates proper symlinks.
|
||||||
|
|
||||||
|
When the ``privacy_level`` changes in the subprojects, we need to
|
||||||
|
re-symlink the superproject also to keep in sync its symlink under the
|
||||||
|
private/public roots.
|
||||||
|
"""
|
||||||
|
filesystem_before = {
|
||||||
|
'private_cname_project': {},
|
||||||
|
'private_cname_root': {},
|
||||||
|
'private_web_root': {
|
||||||
|
'project': {
|
||||||
|
'en': {},
|
||||||
|
},
|
||||||
|
'subproject': {
|
||||||
|
'en': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'public_cname_project': {},
|
||||||
|
'public_cname_root': {},
|
||||||
|
'public_web_root': {
|
||||||
|
'project': {
|
||||||
|
'en': {
|
||||||
|
'latest': {
|
||||||
|
'type': 'link',
|
||||||
|
'target': 'user_builds/project/rtd-builds/latest',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'projects': {
|
||||||
|
'subproject': {
|
||||||
|
'type': 'link',
|
||||||
|
'target': 'public_web_root/subproject',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'subproject': {
|
||||||
|
'en': {
|
||||||
|
'latest': {
|
||||||
|
'type': 'link',
|
||||||
|
'target': 'user_builds/subproject/rtd-builds/latest',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
filesystem_after = {
|
||||||
|
'private_cname_project': {},
|
||||||
|
'private_cname_root': {},
|
||||||
|
'private_web_root': {
|
||||||
|
'project': {
|
||||||
|
'en': {},
|
||||||
|
'projects': {
|
||||||
|
'subproject': {
|
||||||
|
'type': 'link',
|
||||||
|
'target': 'private_web_root/subproject',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'subproject': {
|
||||||
|
'en': {
|
||||||
|
'latest': {
|
||||||
|
'type': 'link',
|
||||||
|
'target': 'user_builds/subproject/rtd-builds/latest',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'public_cname_project': {},
|
||||||
|
'public_cname_root': {},
|
||||||
|
'public_web_root': {
|
||||||
|
'project': {
|
||||||
|
'en': {
|
||||||
|
'latest': {
|
||||||
|
'type': 'link',
|
||||||
|
'target': 'user_builds/project/rtd-builds/latest',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'projects': {},
|
||||||
|
},
|
||||||
|
'subproject': {
|
||||||
|
'en': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertEqual(self.project.subprojects.all().count(), 0)
|
||||||
|
self.assertEqual(self.subproject.superprojects.all().count(), 0)
|
||||||
|
self.project.add_subproject(self.subproject)
|
||||||
|
self.assertEqual(self.project.subprojects.all().count(), 1)
|
||||||
|
self.assertEqual(self.subproject.superprojects.all().count(), 1)
|
||||||
|
|
||||||
|
self.assertTrue(self.project.versions.first().active)
|
||||||
|
self.assertTrue(self.subproject.versions.first().active)
|
||||||
|
symlink_project(self.project.pk)
|
||||||
|
|
||||||
|
self.assertFilesystem(filesystem_before)
|
||||||
|
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
self.client.post(
|
||||||
|
reverse('project_version_detail',
|
||||||
|
kwargs={
|
||||||
|
'project_slug': self.subproject.slug,
|
||||||
|
'version_slug': self.subproject.versions.first().slug,
|
||||||
|
}),
|
||||||
|
data={'privacy_level': 'private', 'active': True},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(self.subproject.versions.first().privacy_level, 'private')
|
||||||
|
self.assertTrue(self.subproject.versions.first().active)
|
||||||
|
|
||||||
|
self.client.post(
|
||||||
|
reverse('projects_advanced',
|
||||||
|
kwargs={
|
||||||
|
'project_slug': self.subproject.slug,
|
||||||
|
}),
|
||||||
|
data={
|
||||||
|
# Required defaults
|
||||||
|
'python_interpreter': 'python',
|
||||||
|
'default_version': 'latest',
|
||||||
|
|
||||||
|
'privacy_level': 'private',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(self.subproject.versions.first().active)
|
||||||
|
self.subproject.refresh_from_db()
|
||||||
|
self.assertEqual(self.subproject.privacy_level, 'private')
|
||||||
|
self.assertFilesystem(filesystem_after)
|
||||||
|
|
|
@ -9,3 +9,4 @@ Mercurial==4.4.2
|
||||||
|
|
||||||
# local debugging tools
|
# local debugging tools
|
||||||
pdbpp
|
pdbpp
|
||||||
|
datadiff
|
||||||
|
|
Loading…
Reference in New Issue