diff --git a/.meta.toml b/.meta.toml index 28cf099..8016d6e 100644 --- a/.meta.toml +++ b/.meta.toml @@ -7,3 +7,6 @@ commit-id = "2.8.1.dev0" [tox] test_matrix = {"6.2" = ["*"]} + +[pyproject] +dependencies_ignores = "['z3c.jbot']" diff --git a/news/3953.feature b/news/3953.feature new file mode 100644 index 0000000..fff245f --- /dev/null +++ b/news/3953.feature @@ -0,0 +1,2 @@ +Move Classic-UI template/view code to plone.app.layout +[petschki] diff --git a/pyproject.toml b/pyproject.toml index d6d4b32..3ab6c9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -122,6 +122,7 @@ Zope = [ ] python-dateutil = ['dateutil'] pytest-plone = ['pytest', 'zope.pytestlayer', 'plone.testing', 'plone.app.testing'] +ignore-packages = ['z3c.jbot'] ## # Add extra configuration options in .meta.toml: diff --git a/setup.py b/setup.py index 5fa5e8e..0f01e20 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,6 @@ "plone.base", "plone.dexterity", "Products.GenericSetup", - "Products.statusmessages", "plone.app.textfield", "plone.app.uuid", "plone.registry", diff --git a/src/plone/app/linkintegrity/__init__.py b/src/plone/app/linkintegrity/__init__.py index e69de29..54f7330 100644 --- a/src/plone/app/linkintegrity/__init__.py +++ b/src/plone/app/linkintegrity/__init__.py @@ -0,0 +1,4 @@ +jbot_deprecations = { + "plone.app.linkintegrity.browser.delete_confirmation_info.pt": "plone.app.layout.controlpanels.templates.linkintegrity_delete_confirmation_info.pt", # noqa: E501 + "plone.app.linkintegrity.browser.update.pt": "plone.app.layout.controlpanels.templates.linkintegrity_update.pt", # noqa: E501 +} diff --git a/src/plone/app/linkintegrity/browser/configure.zcml b/src/plone/app/linkintegrity/browser/configure.zcml index 7931e09..a054b09 100644 --- a/src/plone/app/linkintegrity/browser/configure.zcml +++ b/src/plone/app/linkintegrity/browser/configure.zcml @@ -3,6 +3,12 @@ xmlns:browser="http://namespaces.zope.org/browser" > + - - diff --git a/src/plone/app/linkintegrity/browser/delete_confirmation_info.pt b/src/plone/app/linkintegrity/browser/delete_confirmation_info.pt deleted file mode 100644 index 0b938c0..0000000 --- a/src/plone/app/linkintegrity/browser/delete_confirmation_info.pt +++ /dev/null @@ -1,135 +0,0 @@ - - - -

Potential link breakage

- -
- -

- By deleting this item, you will break links that exist in the items listed - below. If this is indeed what you want to do, we recommend that you remove - these references first. -

- -
-
- - -
-

- - This - - is referenced by the following items: - -

-
- - - -
- -
- -

Deleting overview

-

- - Number of selected, non-empty folders: - - -

-
    -
  • - - Following content within - - will also be deleted: -
    -
      -
    • - - -
    • -
    -
  • -
- -
- -

- Would you like to delete it anyway? -

- -
- -
- - - diff --git a/src/plone/app/linkintegrity/browser/info.py b/src/plone/app/linkintegrity/browser/info.py index 4769bda..8ae44c5 100644 --- a/src/plone/app/linkintegrity/browser/info.py +++ b/src/plone/app/linkintegrity/browser/info.py @@ -9,7 +9,6 @@ from Products.CMFCore.utils import _checkPermission from Products.CMFCore.utils import getToolByName from Products.Five import BrowserView -from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile from zope.i18n import translate import logging @@ -19,7 +18,7 @@ class DeleteConfirmationInfo(BrowserView): - template = ViewPageTemplateFile("delete_confirmation_info.pt") + template = None breach_count = {} def __init__(self, context, request): @@ -37,7 +36,9 @@ def __call__(self, items=None): else: items = [self.context] self.breaches = self.get_breaches(items) - return self.template() + if self.template: + return self.template() + return "" def get_breaches(self, items=None): """Return breaches for multiple items. diff --git a/src/plone/app/linkintegrity/browser/update.pt b/src/plone/app/linkintegrity/browser/update.pt deleted file mode 100644 index f099245..0000000 --- a/src/plone/app/linkintegrity/browser/update.pt +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - -

Update link integrity information

- -
- -

- Clicking the below button will cause link integrity information to be - updated. This might take a while, especially for bigger sites... -

- -
- - -
- -
- -
- - diff --git a/src/plone/app/linkintegrity/browser/update.py b/src/plone/app/linkintegrity/browser/update.py index e190959..fcd6681 100644 --- a/src/plone/app/linkintegrity/browser/update.py +++ b/src/plone/app/linkintegrity/browser/update.py @@ -1,66 +1,7 @@ -from Acquisition import aq_inner -from datetime import datetime -from datetime import timedelta -from plone.app.linkintegrity.handlers import modifiedContent -from plone.base import PloneMessageFactory as _ -from Products.CMFCore.utils import getToolByName -from Products.Five import BrowserView -from Products.statusmessages.interfaces import IStatusMessage -from transaction import savepoint -from zExceptions import NotFound +import zope.deferredimport -import logging - -logger = logging.getLogger(__name__) - - -class UpdateView(BrowserView): - """Iterate over all catalogued items and update linkintegrity-information.""" - - def __call__(self): - context = aq_inner(self.context) - request = aq_inner(self.request) - if "update" in request.form or "delete_all" in request.form: - starttime = datetime.now() - count = self.update() - duration = timedelta(seconds=(datetime.now() - starttime).seconds) - msg = _( - "linkintegrity_update_info", - default="Link integrity information updated for ${count} " - + "items in ${time} seconds.", - mapping={"count": count, "time": str(duration)}, - ) - IStatusMessage(request).add(msg, type="info") - msg = "Updated {} items in {} seconds".format( - count, - str(duration), - ) - logger.info(msg) - request.RESPONSE.redirect(getToolByName(context, "portal_url")()) - elif "cancel" in request.form: - msg = _("Update cancelled.") - IStatusMessage(request).add(msg, type="info") - request.RESPONSE.redirect(getToolByName(context, "portal_url")()) - else: - return self.index() - - def update(self): - catalog = getToolByName(self.context, "portal_catalog") - count = 0 - - for brain in catalog(): - try: - obj = brain.getObject() - except (AttributeError, NotFound, KeyError): - msg = "Catalog inconsistency: {0} not found!" - logger.error(msg.format(brain.getPath()), exc_info=1) - continue - try: - modifiedContent(obj, "dummy event parameter") - count += 1 - except Exception: - msg = "Error updating linkintegrity-info for {0}." - logger.error(msg.format(obj.absolute_url()), exc_info=1) - if count % 1000 == 0: - savepoint(optimistic=True) - return count +zope.deferredimport.initialize() +zope.deferredimport.deprecated( + "Please use from plone.app.layout.controlpanels.linkintegrity import UpdateView", # noqa: E501 + UpdateView="plone.app.layout:controlpanels.linkintegrity.UpdateView", +) diff --git a/src/plone/app/linkintegrity/configure.zcml b/src/plone/app/linkintegrity/configure.zcml index e3fa781..566b4c2 100644 --- a/src/plone/app/linkintegrity/configure.zcml +++ b/src/plone/app/linkintegrity/configure.zcml @@ -2,6 +2,7 @@ xmlns="http://namespaces.zope.org/zope" xmlns:browser="http://namespaces.zope.org/browser" xmlns:genericsetup="http://namespaces.zope.org/genericsetup" + xmlns:zcml="http://namespaces.zope.org/zcml" > @@ -11,6 +12,17 @@ file="permissions.zcml" /> + + + + diff --git a/src/plone/app/linkintegrity/tests/test_circular.py b/src/plone/app/linkintegrity/tests/test_circular.py index 807cc67..5b9383a 100644 --- a/src/plone/app/linkintegrity/tests/test_circular.py +++ b/src/plone/app/linkintegrity/tests/test_circular.py @@ -84,11 +84,7 @@ def test_internal_breaches_are_dropped(self): self.assertEqual(len(view.get_breaches([doc1])), 1) self.assertEqual(len(view.get_breaches([doc1, folder1])), 0) - view = folder1.restrictedTraverse("delete_confirmation") - self.assertIn("Potential link breakage", view()) - view = folder1.restrictedTraverse("delete_confirmation_info") - self.assertIn("Potential link breakage", view()) - view = doc4.restrictedTraverse("delete_confirmation") - self.assertNotIn("Potential link breakage", view()) - view = doc4.restrictedTraverse("delete_confirmation_info") - self.assertNotIn("Potential link breakage", view()) + # HTML rendering is a layout concern (plone.app.layout); test the API: + view = DeleteConfirmationInfo(self.portal, self.request) + self.assertGreater(len(view.get_breaches([folder1])), 0) + self.assertEqual(len(view.get_breaches([doc4])), 0) diff --git a/src/plone/app/linkintegrity/tests/test_functional.py b/src/plone/app/linkintegrity/tests/test_functional.py index 430b397..1e45bd6 100644 --- a/src/plone/app/linkintegrity/tests/test_functional.py +++ b/src/plone/app/linkintegrity/tests/test_functional.py @@ -1,422 +1,61 @@ +"""Functional tests for link integrity that stay in plone.app.linkintegrity. + +Only tests that verify behaviour independent of HTML rendering belong here. +All HTML-rendering tests (delete confirmation page with "Potential link +breakage") live in plone.app.layout.tests.test_linkintegrity. +""" + from plone.app.linkintegrity import testing from plone.app.linkintegrity.tests.utils import set_text -from plone.app.linkintegrity.utils import getIncomingLinks from plone.app.linkintegrity.utils import getOutgoingLinks -from plone.app.linkintegrity.utils import hasOutgoingLinks -from plone.app.testing import setRoles -from plone.app.testing import TEST_USER_ID from plone.app.testing import TEST_USER_NAME from plone.app.testing import TEST_USER_PASSWORD -from plone.base.interfaces import IEditingSchema -from plone.registry.interfaces import IRegistry from plone.testing.zope import Browser -from zc.relation.interfaces import ICatalog -from zope.component import getMultiAdapter -from zope.component import getUtility -import re import transaction import unittest class FunctionalReferenceTestCase(unittest.TestCase): - """functional reference testcase""" + """Functional tests for link integrity behaviour (no HTML assertions).""" layer = testing.PLONE_APP_LINKINTEGRITY_FUNCTIONAL_TESTING def setUp(self): self.portal = self.layer["portal"] self.request = self.layer["request"] - # alsoProvides(self.request, IFormLayer) - # Get a testbrowser self.browser = Browser(self.layer["app"]) self.browser.handleErrors = False self.browser.addHeader("Referer", self.portal.absolute_url()) self.browser.addHeader( - "Authorization", f"Basic {TEST_USER_NAME:s}:{TEST_USER_PASSWORD:s}" + "Authorization", f"Basic {TEST_USER_NAME}:{TEST_USER_PASSWORD}" ) - - # Do an initial page load to make sure the bundles get compiled - # (which currently commits a transaction) - # before we render exception views self.browser.open(self.portal.absolute_url()) - setRoles( - self.portal, - TEST_USER_ID, - [ - "Manager", - ], - ) - - def _get_token(self, obj): - return getMultiAdapter((obj, self.request), name="authenticator").token() - - @unittest.skip( - "Re-enable after https://github.com/plone/plone.app.content/issues/38" - ) # noqa - def test_file_reference_linkintegrity_page_is_shown(self): - doc1 = self.portal.doc1 - file2 = testing.create(self.portal, "File", id="file2", file=testing.GIF) - - self.assertFalse(hasOutgoingLinks(doc1)) - set_text(doc1, 'A File') - self.assertTrue(hasOutgoingLinks(doc1)) - self.assertIn("file2", self.portal.objectIds()) - - token = self._get_token(file2) - self.request["_authenticator"] = token - - # Make changes visible to test browser - transaction.commit() - - self.browser.handleErrors = True - self.browser.addHeader( - "Authorization", f"Basic {TEST_USER_NAME:s}:{TEST_USER_PASSWORD:s}" - ) - - delete_url = "{:s}/delete_confirmation?_authenticator={:s}".format( - file2.absolute_url(), token - ) - - # Try to remove but cancel - self.browser.open(delete_url) - - # Validate text - self.assertIn("Potential link breakage", self.browser.contents) - self.assertIn( - 'Test Page 1', self.browser.contents - ) - self.assertIn("Would you like to delete it anyway?", self.browser.contents) - - # Click cancel button, item should stay in place - # FIXME! This fails in Plone 6 with an internal server error, - # but maybe no longer for the original reasons for which we skip this test. - self.browser.getControl(name="form.buttons.Cancel").click() - self.assertEqual(self.browser.url, file2.absolute_url() + "/view") - self.assertIn("Removal cancelled.", self.browser.contents) - self.assertIn("file2", self.portal.objectIds()) - - # Try to remove and confirm - self.browser.open(delete_url) - self.browser.getControl(name="form.buttons.Delete").click() - self.assertNotIn("file2", self.portal.objectIds()) - def test_unreferenced_removal(self): - # This tests against #6666 and #7784, simple removal of a not - # referenced file, which broke zeo-based installations. - - # We simply use a browser to try to delete a content item. + # Simple removal of a not-referenced item must work without errors + # (regression for #6666 and #7784 which broke ZEO-based installs). self.browser.open(self.portal.doc1.absolute_url()) self.browser.getLink("Delete").click() self.assertIn("Do you really want to delete this item?", self.browser.contents) self.browser.getControl(name="form.buttons.Delete").click() - - # The resulting page should confirm the removal: self.assertIn("Test Page 1 has been deleted", self.browser.contents) self.assertNotIn("doc1", self.portal.objectIds()) - def test_renaming_referenced_item(self): - doc1 = self.portal.doc1 - doc2 = self.portal.doc2 - - # This tests makes sure items that are linked to can still be - # renamed (see the related bug report in #6608). First we need - # to create the necessary links: - set_text(doc1, 'doc2') - self.assertEqual([i.from_object for i in getIncomingLinks(doc2)], [doc1]) - - # Make changes visible to testbrowseropen - transaction.commit() - - # Then we use a browser to rename the referenced image: - self.browser.handleErrors = True - self.browser.open( - "{:s}/object_rename?_authenticator={:s}".format( - doc1.absolute_url(), self._get_token(doc1) - ) - ) - - self.browser.getControl(name="form.widgets.new_id").value = "nuname" - self.browser.getControl(name="form.buttons.Rename").click() - self.assertIn("Renamed 'doc1' to 'nuname'.", self.browser.contents) - transaction.commit() - - self.assertNotIn("doc1", self.portal.objectIds()) - self.assertIn("nuname", self.portal.objectIds()) - self.assertIn(doc1, [i.from_object for i in getIncomingLinks(doc2)]) - - # We simply use a browser to try to delete a content item. - self.browser.open(doc2.absolute_url()) - self.browser.getLink("Delete").click() - self.assertIn("Do you really want to delete this item?", self.browser.contents) - self.assertIn("nuname", self.portal.objectIds()) - # Link breakabe page should be shown - self.assertIn("Potential link breakage", self.browser.contents) - self.assertIn( - 'Test Page 1', - self.browser.contents, - ) - self.browser.getControl(name="form.buttons.Delete").click() - self.assertNotIn("doc2", self.portal.objectIds()) - - def test_removal_in_subfolder(self): - doc1 = self.portal.doc1 - doc2 = self.portal.doc2 - folder1 = self.portal.folder1 - - # This tests ensuring link integrity when removing an referenced - # object contained in a folder that is removed. - set_text(doc1, 'a document') - set_text(doc2, 'a document') - - # Make changes visible to testbrowseropen - transaction.commit() - - # Then we try to delete the folder holding the referenced - # document. Before we can do this we need to prevent the test - # framework from choking on the exception we intentionally - # throw. - self.browser.handleErrors = True - - self.browser.open( - "{:s}/delete_confirmation?_authenticator={:s}".format( - folder1.absolute_url(), self._get_token(folder1) - ) - ) - self.assertIn("Potential link breakage", self.browser.contents) - self.assertIn( - 'Test Page 1', self.browser.contents - ) - self.assertIn( - 'Test Page 2', self.browser.contents - ) - self.browser.getControl(name="form.buttons.Delete").click() - self.assertNotIn("folder1", self.portal.objectIds()) - - def test_removal_with_cookie_auth(self): - doc1 = self.portal.doc1 - doc2 = self.portal.doc2 - - # This tests ensures link integrity working correctly without - # http basic authentication (see the bug report in #6607). - set_text(doc1, 'doc2') - transaction.commit() - - browser = Browser(self.layer["app"]) - browser.handleErrors = True - browser.addHeader("Referer", self.portal.absolute_url()) - browser.open(f"{self.portal.absolute_url():s}/folder_contents") - - # At this point we shouldn't be able to look at the folder - # contents (as an anonymous user): - self.assertIn("login?came_from", browser.url) - - # So we log in via the regular plone login form and additionally check - # that there is no 'authorization' header set afterwards: - browser.getControl(name="__ac_name").value = TEST_USER_NAME - browser.getControl(name="__ac_password").value = TEST_USER_PASSWORD - browser.getControl("Log in").click() - self.assertNotIn("authorization", [h.lower() for h in browser.headers.keys()]) - - # This should lead us back to the "folder contents" listing, - # where we try to delete the referenced document. - browser.open( - "{:s}/delete_confirmation?_authenticator={:s}".format( - doc2.absolute_url(), self._get_token(doc2) - ) - ) - self.assertIn("Potential link breakage", browser.contents) - self.assertIn( - 'Test Page 1', browser.contents - ) - browser.getControl(name="form.buttons.Delete").click() - self.assertNotIn("doc2", self.portal.objectIds()) - - def test_linkintegrity_on_off_switch(self): - doc1 = self.portal.doc1 - doc2 = self.portal.doc2 - - # This tests switching link integrity checking on and off. - set_text(doc1, 'a document') - transaction.commit() - - # This should lead us back to the "folder contents" listing, - # where we try to delete the referenced document. Before we can - # do this we need to prevent the test framework from choking on - # the exception we intentionally throw. - self.browser.handleErrors = True - - self.browser.open( - "{:s}/delete_confirmation?_authenticator={:s}".format( - doc2.absolute_url(), self._get_token(doc2) - ) - ) - self.assertIn("Potential link breakage", self.browser.contents) - self.assertIn( - 'Test Page 1', self.browser.contents - ) - - # Now we turn the switch for link integrity checking off via the site - # properties and try again: - registry = getUtility(IRegistry) - settings = registry.forInterface(IEditingSchema, prefix="plone") - settings.enable_link_integrity_checks = False - transaction.commit() - self.browser.reload() - self.assertNotIn("Potential link breakage", self.browser.contents) - - def test_update(self): - doc1 = self.portal.doc1 - doc2 = self.portal.doc2 - doc4 = self.portal.folder1.doc4 - - # This tests updating link integrity information for all site content, - # i.e. after migrating from a previous version. - set_text(doc1, 'a document') - set_text(doc2, 'a document') - - catalog = getUtility(ICatalog) - rels = [i for i in catalog.findRelations()] - for rel in rels: - catalog.unindex(rel) - - # Just to make sure, we check that there are no references from or to - # these documents at this point: - self.assertEqual([i.to_object for i in getOutgoingLinks(doc1)], []) - self.assertEqual([i.to_object for i in getOutgoingLinks(doc2)], []) - - # An update of link integrity information for all content is triggered - # by browsing a specific url: - transaction.commit() - self.browser.open( - f"{self.portal.absolute_url():s}/updateLinkIntegrityInformation" - ) - self.browser.getControl("Update").click() - self.assertIn("Link integrity information updated for", self.browser.contents) - - # Now the linking documents should hold the correct link integrity - # references: - self.assertEqual( - [i.to_object for i in getOutgoingLinks(doc1)], - [ - doc2, - ], - ) - self.assertEqual( - [i.to_object for i in getOutgoingLinks(doc2)], - [ - doc4, - ], - ) - - def test_references_on_cloned_objects(self): - doc1 = self.portal.doc1 - doc2 = self.portal.doc2 - - # This tests ensures that link integrity is correctly setup when - # cloning an object. - set_text(doc1, 'a document') - - # Next we clone the document: - token = self._get_token(doc1) - self.request["_authenticator"] = token - doc1.restrictedTraverse("object_copy")() - - self.request["_authenticator"] = token - self.portal.restrictedTraverse("object_paste")() - self.assertIn("copy_of_doc1", self.portal) - transaction.commit() - - # Then we try to delete the document linked by the original document - # and its clone. Before we can do this we need to prevent the test - # framework from choking on the exception we intentionally throw. - self.browser.handleErrors = True - - # Now we can continue and "click" the "delete" action. The confirmation - # page should list both documents: - self.browser.open( - "{:s}/delete_confirmation?_authenticator={:s}".format( - doc2.absolute_url(), self._get_token(doc2) - ) - ) - self.assertIn("is referenced by the following items:", self.browser.contents) - self.assertIn("Potential link breakage", self.browser.contents) - self.assertIn( - 'Test Page 1', self.browser.contents - ) - self.assertIn( - 'a document') - - # The document should now have a reference to the file: - self.assertEqual([i.to_object for i in getOutgoingLinks(doc1)], [spaces1]) - transaction.commit() - - # Then we use a browser to try to delete the referenced file. - # Before we can do this we need to prevent the test framework - # from choking on the exception we intentionally throw. - self.browser.handleErrors = True - - self.browser.open( - "{:s}/delete_confirmation?_authenticator={:s}".format( - spaces1.absolute_url(), self._get_token(spaces1) - ) - ) - self.assertIn("Potential link breakage", self.browser.contents) - self.assertIn( - 'Test Page 1', self.browser.contents - ) - self.browser.getControl(name="form.buttons.Delete").click() - self.assertNotIn("some spaces.doc", self.portal.objectIds()) - def test_removal_via_zmi(self): - """Delete via ZMI is no longer protedted!""" + """Delete via ZMI is not protected by link integrity.""" doc1 = self.portal.doc1 doc2 = self.portal.doc2 - # This tests ensuring link integrity when removing an object via - # the ZMI. set_text(doc1, 'a document') self.assertEqual([i.to_object for i in getOutgoingLinks(doc1)], [doc2]) - transaction.commit() - # Then we use a browser to try to delete the referenced - # document. Before we can do this we need to prevent the test - # framework from choking on the exception we intentionally throw. - self.browser.handleErrors = True + self.browser.handleErrors = True self.browser.open("http://nohost/plone/manage_main") self.browser.getControl(name="ids:list").getControl(value="doc2").selected = ( True ) - self.browser.getControl("Delete").click() self.assertNotIn("doc2", self.portal.objectIds()) - - def test_warn_about_content(self): - folder1 = self.portal.folder1 - self.browser.open( - "{:s}/delete_confirmation?_authenticator={:s}".format( - folder1.absolute_url(), self._get_token(folder1) - ) - ) - self.assertIn("Number of selected", self.browser.contents) - self.assertTrue(re.search(r"2\s+Objects in all", self.browser.contents)) - self.assertTrue(re.search(r"1\s+Folders", self.browser.contents)) - self.assertTrue(re.search(r"0\s+Published objects", self.browser.contents)) diff --git a/src/plone/app/linkintegrity/tests/test_references.py b/src/plone/app/linkintegrity/tests/test_references.py index 945b6cd..2ba410a 100644 --- a/src/plone/app/linkintegrity/tests/test_references.py +++ b/src/plone/app/linkintegrity/tests/test_references.py @@ -1,4 +1,5 @@ from plone.app.linkintegrity import testing +from plone.app.linkintegrity.browser.info import DeleteConfirmationInfo from plone.app.linkintegrity.parser import extractLinks from plone.app.linkintegrity.tests.utils import set_text from plone.app.linkintegrity.utils import getIncomingLinks @@ -26,6 +27,7 @@ class ReferenceGenerationTestCase(unittest.TestCase): def setUp(self): self.portal = self.layer["portal"] + self.request = self.layer["request"] def _set_related_items(self, obj, items): assert IRelatedItems.providedBy(obj) @@ -68,12 +70,14 @@ def test_referal_to_private_files(self): self.assertTrue(checkPermission("View", img)) self.assertTrue(checkPermission("Access contents information", img)) - # The warning is shown. + # The API reports the breach; layout concerns (HTML rendering) are + # tested in plone.app.layout. self.assertTrue(hasOutgoingLinks(doc)) - view = img.restrictedTraverse("delete_confirmation") - results = view() - self.assertIn("Potential link breakage", results) - self.assertIn("The item is not accessible.", results) + view = DeleteConfirmationInfo(img, self.request) + breaches = view.get_breaches() + self.assertEqual(len(breaches), 1) + # The linking source is not accessible to the current user (member) + self.assertFalse(breaches[0]["sources"][0]["accessible"]) # delete linked item and check if the source still has the relation