Beispiel #1
0
    def test_before_create(self):

        from woost.models import CreateTrigger, Item

        # Declare the trigger
        trigger, response_log = self.make_trigger(CreateTrigger,
                                                  execution_point="before",
                                                  batch_execution=False)

        qname = "item1"
        item1 = Item(qname=qname)
        assert len(response_log) == 1

        id = 234
        item2 = Item(id=id)
        assert len(response_log) == 2

        response = response_log[0]
        assert response["trigger"] is trigger
        assert response["items"] == [item1]
        assert response["user"] is self.user
        assert not response["batch"]
        assert response["context"]["values"]["qname"] == qname

        response = response_log[1]
        assert response["trigger"] is trigger
        assert response["items"] == [item2]
        assert response["user"] is self.user
        assert not response["batch"]
        assert response["context"]["values"]["id"] == id
Beispiel #2
0
    def test_permission_expression(self):

        from woost.models import (Item, Publishable, User, ReadPermission,
                                  PermissionExpression)

        self.everybody_role.permissions.append(
            ReadPermission(matching_items={
                "type": "woost.models.publishable.Publishable"
            },
                           authorized=True))

        i1 = Item()
        i2 = Item()
        d1 = Publishable()
        d2 = Publishable()

        i1.insert()
        i2.insert()
        d1.insert()
        d2.insert()

        results = set(
            Item.select(
                filters=[PermissionExpression(User(), ReadPermission)]))

        assert results == set([d1, d2])
Beispiel #3
0
    def test_after_insert_modify_delete(self):

        from cocktail.persistence import datastore
        from woost.models import (Item, User, InsertTrigger, ModifyTrigger,
                                  DeleteTrigger)

        response_log = []

        insert_trigger = self.make_trigger(InsertTrigger,
                                           response_log,
                                           execution_point="after",
                                           batch_execution=False)

        modify_trigger = self.make_trigger(ModifyTrigger,
                                           response_log,
                                           execution_point="after",
                                           batch_execution=False)

        delete_trigger = self.make_trigger(DeleteTrigger,
                                           response_log,
                                           execution_point="after",
                                           batch_execution=False)

        # Create and insert an item
        item = Item()
        item.qname = "foo"
        item.insert()

        # Modify it
        item.qname = "bar"
        item.global_id = "foobar"

        # Delete it
        item.delete()

        # Commit the transaction; this should execute all the scheduled
        # responses
        datastore.commit()
        assert len(response_log) == 4

        response = response_log.pop(0)
        assert response["trigger"] is insert_trigger
        assert response["items"] == [item]
        assert response["user"] is self.user
        assert not response["batch"]

        for member in (Item.qname, Item.global_id):
            response = response_log.pop(0)
            assert response["trigger"] is modify_trigger
            assert response["items"] == [item]
            assert response["context"]["member"] is member
            assert response["user"] is self.user
            assert not response["batch"]

        response = response_log[0]
        assert response["trigger"] is delete_trigger
        assert response["items"] == [item]
        assert response["user"] is self.user
        assert not response["batch"]
Beispiel #4
0
    def test_language(self):

        from woost.models import ModifyTrigger, Item

        self.assert_match(ModifyTrigger(matching_languages=["en", "fr"]),
                          (Item(), None, {
                              "language": "en"
                          }, True), (Item(), None, {
                              "language": "fr"
                          }, True), (Item(), None, {
                              "language": None
                          }, False), (Item(), None, {
                              "language": "ru"
                          }, False))
Beispiel #5
0
    def test_after_insert(self):

        from cocktail.persistence import datastore
        from woost.models import InsertTrigger, Item

        # Declare the trigger
        trigger, response_log = self.make_trigger(InsertTrigger,
                                                  execution_point="after",
                                                  batch_execution=False)

        # Insert a new item, but abort the transaction
        # (the trigger shouldn't be called)
        item = Item()
        item.insert()
        assert not response_log
        datastore.abort()
        assert not response_log

        # Insert a new item, and commit the transaction
        # (the trigger should be executed)
        item = Item()
        item.insert()
        datastore.commit()

        assert len(response_log) == 1
        response = response_log[0]
        assert response["trigger"] is trigger
        assert response["items"] == [item]
        assert response["user"] is self.user
        assert not response["batch"]
Beispiel #6
0
    def test_after_delete(self):

        from cocktail.persistence import datastore
        from woost.models import DeleteTrigger, Item

        # Declare the trigger
        trigger, response_log = self.make_trigger(DeleteTrigger,
                                                  execution_point="after",
                                                  batch_execution=False)

        # Create and insert an item
        item = Item()
        item.insert()
        datastore.commit()

        # Delete the item, but abort the transaction. This shouldn't trigger
        # the response.
        item.delete()
        datastore.abort()
        assert not response_log

        # Delete the inserted item and commit the transaction. This should
        # trigger the response.
        item.delete()
        datastore.commit()

        assert len(response_log) == 1
        response = response_log[0]
        assert response["trigger"] is trigger
        assert response["items"] == [item]
        assert response["user"] is self.user
        assert not response["batch"]
Beispiel #7
0
    def test_before_insert(self):

        from woost.models import InsertTrigger, Item

        # Declare the trigger
        trigger, response_log = self.make_trigger(InsertTrigger,
                                                  execution_point="before",
                                                  batch_execution=False)

        # Create two items. This should trigger the response twice.
        item1 = Item()
        item1.insert()
        assert len(response_log) == 1

        item2 = Item()
        item2.insert()
        assert len(response_log) == 2

        response = response_log[0]
        assert response["trigger"] is trigger
        assert response["items"] == [item1]
        assert response["user"] is self.user
        assert not response["batch"]

        response = response_log[1]
        assert response["trigger"] is trigger
        assert response["items"] == [item2]
        assert response["user"] is self.user
        assert not response["batch"]
Beispiel #8
0
    def test_user(self):

        from woost.models import ContentTrigger, Item, Role, User

        r1 = Role()
        r2 = Role()
        r3 = Role(base_roles=[r2])

        u1 = User(roles=[r1])
        u2 = User(roles=[r2])
        u3 = User(roles=[r3])

        self.assert_match(ContentTrigger(matching_roles=[r2]),
                          (Item(), u2, {}, True), (Item(), u3, {}, True),
                          (Item(), None, {}, False), (Item(), u1, {}, False))
    def __call__(self, *args, **kwargs):

        rel = cherrypy.request.params.get("relation-select")

        # Open the item selector
        if rel:

            # Load persistent collection parameters before redirecting
            self.user_collection

            pos = rel.find("-")
            root_content_type_name = rel[:pos]
            selection_parameter = str(rel[pos + 1:])

            for content_type in chain([Item], Item.derived_schemas()):
                if content_type.full_name == root_content_type_name:

                    edit_stacks_manager = self.context["edit_stacks_manager"]
                    edit_stack = edit_stacks_manager.current_edit_stack

                    if edit_stack is None:
                        edit_stack = edit_stacks_manager.create_edit_stack()
                        edit_stacks_manager.current_edit_stack = edit_stack

                    node = SelectionNode()
                    node.content_type = content_type
                    node.selection_parameter = selection_parameter
                    edit_stack.push(node)
                    raise cherrypy.HTTPRedirect(
                        node.uri(
                            selection=self.params.read(
                                schema.String(selection_parameter)),
                            client_side_scripting=self.client_side_scripting))

        return BaseBackOfficeController.__call__(self, **kwargs)
Beispiel #10
0
def remove_drafts(e):

    from cocktail.persistence import datastore
    from woost.models import (Item, Permission, ContentPermission,
                              MemberPermission, Trigger, ContentTrigger)
    from woost.models.utils import remove_broken_type

    remove_broken_type("woost.models.permission.ConfirmDraftPermission",
                       existing_bases=(Item, Permission, ContentPermission))

    remove_broken_type("woost.models.permission.ConfirmDraftPermission",
                       existing_bases=(Item, Trigger, ContentTrigger))

    for item in Item.select():
        for key in "_is_draft", "_draft_source", "_drafts":
            try:
                delattr(item, key)
            except AttributeError:
                pass

    datastore.root.pop("woost.models.item.Item.is_draft", None)
    datastore.root.pop("woost.models.item.Item.draft_source", None)

    for permission in MemberPermission.select():
        for key in ("drafts", "draft_source", "is_draft"):
            key = "woost.models.item.Item." + key
            if key in permission.matching_members:
                permission.matching_members.remove(key)
Beispiel #11
0
    def get_produced_member(self):

        full_name = self.package_name + "." + self.member_name

        for cls in Item.schema_tree():
            if cls.full_name == full_name:
                return cls
Beispiel #12
0
    def resolve(self, path):

        try:
            self.edited_item = Item.require_instance(int(path.pop(0)))
        except:
            raise cherrypy.HTTPError(400)

        return self
Beispiel #13
0
    def test_modified_member(self):

        from woost.models import ModifyTrigger, Item, Document

        self.assert_match(
            ModifyTrigger(matching_members=[
                "woost.models.item.Item.qname",
                "woost.models.document.Document.title"
            ]), (Item(), None, {
                "member": Item.qname
            }, True), (Item(), None, {
                "member": Item.global_id
            }, False), (Document(), None, {
                "member": Document.title
            }, True), (Document(), None, {
                "member": Document.hidden
            }, False))
Beispiel #14
0
    def form_adapter(self):

        adapter = FormControllerMixin.form_adapter(self)
        adapter.exclude([
            "prefered_language", "roles", "password_confirmation", "enabled",
            "confirmed_email"
        ] + [key for key in Item.members()])
        return adapter
Beispiel #15
0
def assign_global_identifiers(e):
    from woost import app
    from woost.models import Item
    from woost.models.synchronization import rebuild_manifest

    for item in Item.select():
        item._global_id = app.installation_id + "-" + str(item.id)

    Item.global_id.rebuild_index()
    Item.synchronizable.rebuild_index()
    rebuild_manifest(True)
Beispiel #16
0
    def test_delete(self):

        from datetime import datetime
        from woost.models import (Item, User, ChangeSet, changeset_context)

        author = User()
        author.insert()

        item = Item()
        item.insert()

        with changeset_context(author) as changeset:
            item.delete()

        assert list(ChangeSet.select()) == [changeset]
        assert changeset.author is author
        assert isinstance(changeset.date, datetime)

        assert changeset.changes.keys() == [item.id]
        change = changeset.changes[item.id]
        assert change.target is item
        assert change.action == "delete"
        assert change.changeset is changeset
        assert item.changes == [change]

        assert not item.id in Item.index
Beispiel #17
0
    def __setstate__(self, state):

        EditNode.__setstate__(self, state)

        self.block_parent = Item.get_instance(state["block_parent"])

        if self.block_parent:
            block_type = type(self.block_parent)
            self.block_slot = block_type.get_member(state["block_slot"])

        anchor_id = state.get("block_anchor")
        if anchor_id is not None:
            self.block_anchor = Block.get_instance(anchor_id)
Beispiel #18
0
    def test_content_permissions(self):

        from woost.models import (Item, Publishable, User, Role, Permission,
                                  CreatePermission, ReadPermission,
                                  ModifyPermission, DeletePermission)

        class TestPermission(Permission):
            pass

        item = Item()
        doc = Publishable()

        for permission_type in [
                CreatePermission, ReadPermission, ModifyPermission,
                DeletePermission
        ]:
            role = Role()
            user = User(roles=[role])

            # Permission denied (no permissions defined)
            self.assert_not_authorized(user, permission_type, target=doc)

            # Permission granted
            role.permissions.append(
                permission_type(matching_items={
                    "type": "woost.models.publishable.Publishable"
                },
                                authorized=True))

            self.assert_authorized(user, permission_type, target=doc)

            # Permission denied (wrong target)
            self.assert_not_authorized(user, permission_type, target=item)

            # Permission denied (wrong permission type)
            self.assert_not_authorized(user, TestPermission)

            # Permission denied (prevailing negative permission)
            role.permissions.insert(
                0,
                permission_type(
                    matching_items={"type": "woost.models.item.Item"},
                    authorized=False))

            self.assert_not_authorized(user, permission_type, target=doc)
Beispiel #19
0
    def invoke(self, controller, selection):
        clipboard = get_block_clipboard_contents()

        if not clipboard:
            notify_user(
                translations("woost.block_clipboard.empty"),
                "error"
            )
        else:
            try:
                block = Block.require_instance(clipboard["block"])
                src_parent = Item.require_instance(clipboard["block_parent"])
                src_slot = type(src_parent).get_member(clipboard["block_slot"])
            except:
                notify_user(
                    translations("woost.block_clipboard.error"),
                    "error"
                )
            else:
                # Remove the block from the source location
                if clipboard["mode"] == "cut":
                    if isinstance(src_slot, schema.Reference):
                        src_parent.set(src_slot, None)
                    elif isinstance(src_slot, schema.Collection):
                        src_collection = src_parent.get(src_slot)
                        schema.remove(src_collection, block)
                # Or copy it
                elif clipboard["mode"] == "copy":
                    block = block.create_copy()
                    block.insert()

                # Add the block to its new position
                add_block(
                    block, 
                    controller.block_parent,
                    controller.block_slot,
                    positioning = self.block_positioning,
                    anchor = controller.block
                )

                datastore.commit()
                del session[BLOCK_CLIPBOARD_SESSION_KEY]
                focus_block(block)
Beispiel #20
0
def remove_owner_field(e):

    from cocktail.persistence import datastore
    from woost.models import Item, ContentPermission, MemberPermission

    # Remove the owner value of every item
    for item in Item.select():
        try:
            del item._owner
        except AttributeError:
            pass

    # Drop the index for the member
    full_member_name = "woost.models.item.Item.owner"
    datastore.root.pop(full_member_name, None)

    # Purge all references to the owner member from member permissions
    for permission in MemberPermission.select():
        if permission.matching_members:
            member_count = len(permission.matching_members)
            try:
                permission.matching_members.remove(full_member_name)
            except (KeyError, ValueError):
                pass
            else:
                if member_count == 1:
                    permission.delete()

    # Purge the 'owned-items' expression from all permissions
    for permission in ContentPermission.select():
        matching_items = permission.matching_items
        if matching_items:
            try:
                filter = permission.matching_items["filter"]
            except KeyError:
                pass
            else:
                if filter == "owned-items" or (hasattr(filter, "__contains__")
                                               and "owned-items" in filter):
                    permission.delete()
Beispiel #21
0
    def test_target(self):

        from woost.models import (ContentTrigger, Item, Publishable,
                                  StandardPage, User, set_current_user)

        self.assert_match(
            ContentTrigger(matching_items={
                "type": "woost.models.publishable.Publishable"
            }), (Publishable(), None, {}, True),
            (StandardPage(), None, {}, True), (Item(), None, {}, False),
            (ContentTrigger(), None, {}, False))

        user = User()
        set_current_user(user)

        self.assert_match(
            ContentTrigger(
                matching_items={
                    "type": "woost.models.publishable.Publishable",
                    "filter": "member-qname",
                    "filter_operator0": "eq",
                    "filter_value0": "foobar"
                }), (Publishable(), user, {}, False),
            (Publishable(qname="foobar"), user, {}, True))
Beispiel #22
0
    def test_add_filters_from_selector(self):

        from cocktail.schema.expressions import Self
        from cocktail.persistence import Query

        results = Item.select(
            [Item.id.greater(15),
             Self.search("@localhost", languages=["en"])])
        results_count = len(results)

        browser.open("/en/cms/content/?content_view=flat&search_expanded=true"
                     "&page_size=%d" % (results_count + 1))
        admin_login()

        # Add filters
        for i, filter_id in enumerate(("member-id", "global_search")):
            browser.fire_event("css=.new_filter_selector", "click")
            browser.fire_event("css=.new_filter-%s" % filter_id, "click")

        assert browser.jquery_count(".filters .filter_list .filter_entry") == 2

        # Set values on filters
        browser.type("filter_operator0", "gt")
        browser.type("filter_value0", "15")
        browser.type("filter_value1", "@localhost")
        browser.click("css=.filters .search_button")
        browser.wait_for_page_to_load(10000)

        # Test the returned content
        rows_count = browser.jquery_count(".collection_display .item_row")
        assert rows_count == results_count

        # Supplied values must be preserved between page loads
        assert browser.get_selected_value("filter_operator0") == "gt"
        assert browser.get_value("filter_value0") == "15"
        assert browser.get_value("filter_value1") == "@localhost"
Beispiel #23
0
    def test_before_modify(self):

        from woost.models import ModifyTrigger, Item, User

        # Declare the trigger
        trigger, response_log = self.make_trigger(ModifyTrigger,
                                                  execution_point="before",
                                                  batch_execution=False)

        # Create an item and initialize it. This shouldn't trigger any
        # response, since modifications happen before the item is inserted.
        item = Item()
        item.qname = "foo"
        item.insert()
        assert not response_log

        # Modify the inserted item two times. This should trigger the response
        # twice.
        item.qname = "bar"
        assert len(response_log) == 1

        item.global_id = "foobar"
        assert len(response_log) == 2

        response = response_log[0]
        assert response["trigger"] is trigger
        assert response["items"] == [item]
        assert response["context"]["member"] is Item.qname
        assert response["user"] is self.user
        assert not response["batch"]

        response = response_log[1]
        assert response["trigger"] is trigger
        assert response["items"] == [item]
        assert response["context"]["member"] is Item.global_id
        assert response["user"] is self.user
        assert not response["batch"]
Beispiel #24
0
    def test_after_modify(self):

        from cocktail.persistence import datastore
        from woost.models import ModifyTrigger, Item

        # Declare the trigger
        trigger, response_log = self.make_trigger(ModifyTrigger,
                                                  execution_point="after",
                                                  batch_execution=False)

        # Create an item and initialize it. This shouldn't trigger any
        # response, since modifications happen before the item is inserted.
        item = Item()
        item.qname = "foo"
        item.insert()
        datastore.commit()
        assert not response_log

        # Modify the inserted item, but abort the transaction. Again, this
        # shouldn't trigger any response.
        item.qname = "bar"
        datastore.abort()
        assert not response_log

        # Modify the inserted item and commit the transaction. This should
        # trigger the response.
        item.qname = "spam"
        datastore.commit()

        assert len(response_log) == 1
        response = response_log[0]
        assert response["trigger"] is trigger
        assert response["items"] == [item]
        assert response["context"]["member"] is Item.qname
        assert response["user"] is self.user
        assert not response["batch"]
Beispiel #25
0
    def test_modify_batched_order(self):

        from cocktail.persistence import datastore
        from woost.models import ModifyTrigger, Item, User

        trigger, response_log = self.make_trigger(
            ModifyTrigger,
            execution_point="after",
            batch_execution=True,
            matching_members=["woost.models.item.Item.global_id"])

        # Modifying a member that is not covered by the trigger should alter
        # the context passed to responses, even if it is modified before the
        # member that actions the response
        item = Item()
        item.insert()
        item.qname = "foo"
        item.global_id = "x1"
        datastore.commit()

        response = response_log[0]
        assert response["context"]["modified_members"] == {
            item: set([(Item.qname, None), (Item.global_id, None)])
        }
 def __init__(self, *args, **kwargs):
     Item.__init__(self, *args, **kwargs)
     self.__last_export_hashes = PersistentMapping()
Beispiel #27
0
def add_multisite_support(e):
    from cocktail.persistence import datastore
    from woost.models import Configuration, Website, Item
    root = datastore.root

    # Remove all back-references from the Site and Language models
    for item in Item.select():
        for key in dir(item):
            if (key == "_site" or key.startswith("_Site_")
                    or key.startswith("_Language_")):
                delattr(item, key)

    # Remove the instance of Site from the database
    site_id = list(Item.qname.index.values(key="woost.main_site"))[0]
    site = Item.index[site_id]
    site_state = site.__Broken_state__.copy()
    site_state["translations"] = dict(
        (lang, translation.__Broken_state__.copy())
        for lang, translation in site_state.pop("_translations").iteritems())
    Item.index.remove(site_id)
    Item.keys.remove(site_id)

    # Create the configuration object
    config = Configuration()
    config.qname = "woost.configuration"
    config.insert()

    # Create a website
    website = Website()
    website.insert()
    website.hosts = ["localhost"]
    config.websites.append(website)

    # Languages
    published_languages = []

    for lang_id in root["woost.models.language.Language-keys"]:
        language = Item.index[lang_id]
        Item.index.remove(lang_id)
        Item.keys.remove(lang_id)
        language_state = language.__Broken_state__
        config.languages.append(language_state["_iso_code"])
        if language_state["_enabled"]:
            published_languages.append(language_state["_iso_code"])

    if list(config.languages) != published_languages:
        config.published_languages = published_languages

    # Delete old indexes from the database
    for key in list(root):
        if (key.startswith("woost.models.site.Site")
                or key.startswith("woost.models.language.Language")):
            del root[key]

    # Settings that now belong in Configuration, as attributes
    config.secret_key = site_state.pop("secret_key")

    # Settings that now belong in Configuration, as regular fields
    for key in ("login_page", "generic_error_page", "not_found_error_page",
                "forbidden_error_page", "default_language",
                "backoffice_language", "heed_client_language", "timezone",
                "smtp_host", "smtp_user", "smtp_password"):
        config.set(key, site_state.pop("_" + key))

    # Settings that now belong in Configuration, as collections
    for key in ("publication_schemes", "caching_policies", "renderers",
                "image_factories", "triggers"):
        config.set(key, list(site_state.pop("_" + key)))

    # Settings that now belong in Website, becoming translated fields
    for key in ("town", "region", "country"):
        value = site_state.pop("_" + key)
        for lang in config.languages:
            website.set(key, value, lang)

    # Settings that now belong in website, as translated fields
    for key in ("site_name", "organization_name", "keywords", "description"):
        for lang, translation_state in site_state["translations"].iteritems():
            value = translation_state.pop("_" + key)
            website.set(key, value, lang)

    # Settings that now belong in website, as regular fields
    for key in ("logo", "icon", "home", "organization_url", "address",
                "postal_code", "phone_number", "fax_number", "email",
                "https_policy", "https_persistence"):
        website.set(key, site_state.pop("_" + key))

    # Extension specific changes
    from woost.extensions.blocks import BlocksExtension
    if BlocksExtension.instance.enabled:
        config.common_blocks = list(site_state.pop("_common_blocks"))

    from woost.extensions.audio import AudioExtension
    if AudioExtension.instance.enabled:
        config.audio_encoders = list(site_state.pop("_audio_encoders"))
        config.audio_decoders = list(site_state.pop("_audio_decoders"))

    from woost.extensions.mailer import MailerExtension
    if MailerExtension.instance.enabled:
        from woost.extensions.mailer.mailing import Mailing
        for mailing in Mailing.select():
            language = mailing._language
            if language:
                mailing._language = language.__Broken_state__["_iso_code"]

    from woost.extensions.googleanalytics import GoogleAnalyticsExtension
    if GoogleAnalyticsExtension.instance.enabled:
        account = GoogleAnalyticsExtension.instance._account
        del GoogleAnalyticsExtension.instance._account
        config.google_analytics_account = account

    # Rebuild all indexes
    Item.rebuild_indexes()

    # Preserve the remaining state
    datastore.root["woost.models.migration.multisite_leftovers"] = site_state
Beispiel #28
0
def rebuild_full_text_index(e):
    from woost.models import Item
    Item.rebuild_full_text_indexes(True)
Beispiel #29
0
    def test_after_insert_batched(self):

        from cocktail.persistence import datastore
        from woost.models import InsertTrigger, Item

        # Declare the trigger
        trigger, response_log = self.make_trigger(InsertTrigger,
                                                  execution_point="after",
                                                  batch_execution=True)

        # Insert new items, but abort the transaction
        # (the trigger shouldn't be called)
        item1 = Item()
        item1.insert()
        assert not response_log

        item2 = Item()
        item2.insert()
        assert not response_log

        datastore.abort()
        assert not response_log

        # Create and insert two items, and commit the transaction. The response
        # should be triggered just once.
        item1 = Item()
        item1.insert()
        item2 = Item()
        item2.insert()
        datastore.commit()

        assert len(response_log) == 1

        response = response_log[0]
        assert response["trigger"] is trigger
        assert response["items"] == set([item1, item2])
        assert response["user"] is self.user
        assert response["batch"]
Beispiel #30
0
    def test_after_modify_batched(self):

        from cocktail.persistence import datastore
        from woost.models import ModifyTrigger, Item, User

        # Declare the trigger
        trigger, response_log = self.make_trigger(ModifyTrigger,
                                                  execution_point="after",
                                                  batch_execution=True)

        # Create two items and initialize them. This shouldn't trigger any
        # response, since modifications happen before items are inserted.
        item1 = Item()
        item1.qname = "foo"
        item1.insert()
        item2 = Item()
        item2.global_id = "x1"
        item2.insert()
        datastore.commit()
        assert not response_log

        # Modify the inserted items, but abort the transaction. Again, this
        # shouldn't trigger any response.
        item1.qname = "bar"
        item2.global_id = "x2"
        datastore.abort()
        assert not response_log

        # Modify the inserted items and commit the transaction. This should
        # trigger the response just once.
        item1.qname = "spam"
        item2.global_id = "x3"
        datastore.commit()

        assert len(response_log) == 1
        response = response_log[0]
        assert response["trigger"] is trigger
        assert response["items"] == set([item1, item2])
        assert response["context"]["modified_members"] == {
            item1: set([(Item.qname, None)]),
            item2: set([(Item.global_id, None)])
        }
        assert response["user"] is self.user
        assert response["batch"]