Beispiel #1
0
    def test_unit__crud_caller__ok__user_role_in_workspace(self, session):

        hook = UserRoleInWorkspaceHookImpl()
        session.context.plugin_manager.register(hook)

        owner = User(email="john")
        workspace = Workspace(label="Hello", owner=owner)
        session.add(workspace)
        session.flush()

        role = UserRoleInWorkspace(role=UserRoleInWorkspace.READER,
                                   user=owner,
                                   workspace=workspace)
        session.add(role)
        session.flush()
        hook.mock_hooks.assert_called_with("created",
                                           role=role,
                                           context=session.context)

        role.role = UserRoleInWorkspace.WORKSPACE_MANAGER
        session.add(role)
        session.flush()
        hook.mock_hooks.assert_called_with("modified",
                                           role=role,
                                           context=session.context)

        session.delete(role)
        session.flush()
        hook.mock_hooks.assert_called_with("deleted",
                                           role=role,
                                           context=session.context)
Beispiel #2
0
    def test__unit__CandidateWorkspaceRoleChecker__err_role_insufficient(self):

        current_user = User(user_id=2, email='*****@*****.**')
        current_user.groups.append(
            Group(group_id=2, group_name=Group.TIM_MANAGER_GROUPNAME))
        candidate_workspace = Workspace(workspace_id=3)
        role = UserRoleInWorkspace(user_id=2, workspace_id=3, role=2)
        self.session.add(current_user)
        self.session.add(candidate_workspace)
        self.session.add(role)
        self.session.flush()
        transaction.commit()

        class FakeTracimContext(TracimContext):
            @property
            def current_user(self):
                return current_user

            @property
            def candidate_workspace(self):
                return candidate_workspace

        assert CandidateWorkspaceRoleChecker(2).check(FakeTracimContext())
        with pytest.raises(InsufficientUserRoleInWorkspace):
            CandidateWorkspaceRoleChecker(3).check(FakeTracimContext())
        with pytest.raises(InsufficientUserRoleInWorkspace):
            CandidateWorkspaceRoleChecker(4).check(FakeTracimContext())
Beispiel #3
0
    def test__unit__CandidateWorkspaceRoleChecker__ok__nominal_case(
            self, session):

        current_user = User(user_id=2, email="*****@*****.**")
        current_user.profile = Profile.TRUSTED_USER
        candidate_workspace = Workspace(workspace_id=3, owner=current_user)
        role = UserRoleInWorkspace(user_id=2, workspace_id=3, role=5)
        session.add(current_user)
        session.add(candidate_workspace)
        session.add(role)
        session.flush()
        transaction.commit()

        class FakeBaseFakeTracimContext(BaseFakeTracimContext):
            @property
            def current_user(self):
                return current_user

            @property
            def candidate_workspace(self):
                return candidate_workspace

        assert CandidateWorkspaceRoleChecker(1).check(
            FakeBaseFakeTracimContext())
        assert CandidateWorkspaceRoleChecker(2).check(
            FakeBaseFakeTracimContext())
Beispiel #4
0
    def test__unit__CandidateWorkspaceRoleChecker__err_role_insufficient(
            self, session):

        current_user = User(user_id=2, email="*****@*****.**")
        current_user.profile = Profile.TRUSTED_USER
        candidate_workspace = Workspace(workspace_id=3, owner=current_user)
        role = UserRoleInWorkspace(user_id=2, workspace_id=3, role=2)
        session.add(current_user)
        session.add(candidate_workspace)
        session.add(role)
        session.flush()
        transaction.commit()

        class FakeBaseFakeTracimContext(BaseFakeTracimContext):
            @property
            def current_user(self):
                return current_user

            @property
            def candidate_workspace(self):
                return candidate_workspace

        assert CandidateWorkspaceRoleChecker(2).check(
            FakeBaseFakeTracimContext())
        with pytest.raises(InsufficientUserRoleInWorkspace):
            CandidateWorkspaceRoleChecker(3).check(FakeBaseFakeTracimContext())
        with pytest.raises(InsufficientUserRoleInWorkspace):
            CandidateWorkspaceRoleChecker(4).check(FakeBaseFakeTracimContext())
Beispiel #5
0
class RoleUpdateSchema(marshmallow.Schema):
    role = marshmallow.fields.String(
        required=True,
        example='contributor',
        validate=OneOf(UserRoleInWorkspace.get_all_role_slug()))

    @post_load
    def make_role(self, data: typing.Dict[str, typing.Any]) -> object:
        return RoleUpdate(**data)
Beispiel #6
0
    def create_one(
        self,
        user: User,
        workspace: Workspace,
        role_level: int,
        with_notif: bool,
        flush: bool=True
    ) -> UserRoleInWorkspace:

        # INFO - G.M - 2018-10-29 - Check if role already exist
        query = self._get_one_rsc(user.user_id, workspace.workspace_id)
        if query.count() > 0:
            raise RoleAlreadyExistError(
                'Role already exist for user {} in workspace {}.'.format(
                    user.user_id,
                    workspace.workspace_id
                )
            )
        role = UserRoleInWorkspace()
        role.user_id = user.user_id
        role.workspace = workspace
        role.role = role_level
        role.do_notify = with_notif
        if flush:
            self._session.flush()
        return role
Beispiel #7
0
    def update_role(
        self,
        role: UserRoleInWorkspace,
        role_level: int,
        with_notif: typing.Optional[bool] = None,
        save_now: bool = False,
    ):
        """
        Update role of user in this workspace
        :param role: UserRoleInWorkspace object
        :param role_level: level of new role wanted
        :param with_notif: is user notification enabled in this workspace ?
        :param save_now: database flush
        :return: updated role
        """
        role.role = role_level
        if with_notif is not None:
            role.do_notify = with_notif
        if save_now:
            self.save(role)

        return role
Beispiel #8
0
    def update_role(
        self,
        role: UserRoleInWorkspace,
        role_level: int,
        with_notif: typing.Optional[bool] = None,
        save_now: bool=False,
    ):
        """
        Update role of user in this workspace
        :param role: UserRoleInWorkspace object
        :param role_level: level of new role wanted
        :param with_notif: is user notification enabled in this workspace ?
        :param save_now: database flush
        :return: updated role
        """
        role.role = role_level
        if with_notif is not None:
            role.do_notify = with_notif
        if save_now:
            self.save(role)

        return role
Beispiel #9
0
    def test__unit__ContentTypeCreationChecker__err__implicit_insufficent_role_in_workspace(
            self):

        current_user = User(user_id=2, email="*****@*****.**")
        current_user.groups.append(
            Group(group_id=2, group_name=Group.TIM_MANAGER_GROUPNAME))
        current_workspace = Workspace(workspace_id=3)
        candidate_content_type = ContentType(
            slug="test",
            fa_icon="",
            hexcolor="",
            label="Test",
            creation_label="Test",
            available_statuses=[],
            minimal_role_content_creation=WorkspaceRoles.CONTENT_MANAGER,
        )
        role = UserRoleInWorkspace(user_id=2,
                                   workspace_id=3,
                                   role=WorkspaceRoles.CONTRIBUTOR.level)
        self.session.add(current_user)
        self.session.add(current_workspace)
        self.session.add(role)
        self.session.flush()
        transaction.commit()

        class FakeContentTypeList(object):
            def get_one_by_slug(self, slug=str) -> ContentType:
                return candidate_content_type

        class FakeTracimContext(TracimContext):
            @property
            def current_user(self):
                return current_user

            @property
            def current_workspace(self):
                return current_workspace

            @property
            def candidate_content_type(self):
                return candidate_content_type

        with pytest.raises(InsufficientUserRoleInWorkspace):
            assert ContentTypeCreationChecker(FakeContentTypeList()).check(
                FakeTracimContext())
Beispiel #10
0
    def _build_context_for_content_update(
        self,
        role: UserRoleInWorkspace,
        content_in_context: ContentInContext,
        parent_in_context: typing.Optional[ContentInContext],
        workspace_in_context: WorkspaceInContext,
        actor: User,
        translator: Translator,
    ):

        _ = translator.get_translation
        content = content_in_context.content
        action = content.get_last_action().id
        previous_revision = content.get_previous_revision()
        new_status = _(content.get_status().label)
        workspace_url = workspace_in_context.frontend_url
        role_label = role.role_as_label()
        logo_url = get_email_logo_frontend_url(self.config)

        # FIXME: remove/readapt assert to debug easily broken case
        # assert user
        # assert workspace
        # assert main_title
        # assert status_label
        # # assert status_icon_url
        # assert role_label
        # # assert content_intro
        # assert content_text or content_text == content.description
        # assert logo_url

        return {
            "user": role.user,
            "actor": actor,
            "action": action,
            "workspace": role.workspace,
            "ActionDescription": ActionDescription,
            "parent_in_context": parent_in_context,
            "content_in_context": content_in_context,
            "workspace_url": workspace_url,
            "previous_revision": previous_revision,
            "new_status": new_status,
            "role_label": role_label,
            "logo_url": logo_url,
        }
Beispiel #11
0
    def test__unit__ContentTypeCreationChecker__err__implicit_insufficent_role_in_workspace(
            self, session):

        current_user = User(user_id=2, email="*****@*****.**")
        current_user.profile = Profile.TRUSTED_USER
        current_workspace = Workspace(workspace_id=3, owner=current_user)
        candidate_content_type = TracimContentType(
            slug="test",
            fa_icon="",
            label="Test",
            creation_label="Test",
            available_statuses=[],
            minimal_role_content_creation=WorkspaceRoles.CONTENT_MANAGER,
        )
        role = UserRoleInWorkspace(user_id=2,
                                   workspace_id=3,
                                   role=WorkspaceRoles.CONTRIBUTOR.level)
        session.add(current_user)
        session.add(current_workspace)
        session.add(role)
        session.flush()
        transaction.commit()

        class FakeContentTypeList(object):
            def get_one_by_slug(self, slug=str) -> TracimContentType:
                return candidate_content_type

        class FakeBaseFakeTracimContext(BaseFakeTracimContext):
            @property
            def current_user(self):
                return current_user

            @property
            def current_workspace(self):
                return current_workspace

            @property
            def candidate_content_type(self):
                return candidate_content_type

        with pytest.raises(InsufficientUserRoleInWorkspace):
            assert ContentTypeCreationChecker(FakeContentTypeList()).check(
                FakeBaseFakeTracimContext())
Beispiel #12
0
    def test__unit__ContentTypeCreationChecker__ok__explicit(self):

        current_user = User(user_id=2, email='*****@*****.**')
        current_user.groups.append(
            Group(group_id=2, group_name=Group.TIM_MANAGER_GROUPNAME))
        current_workspace = Workspace(workspace_id=3)
        candidate_content_type = ContentType(
            slug='test',
            fa_icon='',
            hexcolor='',
            label='Test',
            creation_label='Test',
            available_statuses=[],
            minimal_role_content_creation=WorkspaceRoles.CONTENT_MANAGER)
        role = UserRoleInWorkspace(user_id=2,
                                   workspace_id=3,
                                   role=WorkspaceRoles.CONTENT_MANAGER.level)
        self.session.add(current_user)
        self.session.add(current_workspace)
        self.session.add(role)
        self.session.flush()
        transaction.commit()

        class FakeContentTypeList(object):
            def get_one_by_slug(self, slug=str) -> ContentType:
                return candidate_content_type

        class FakeTracimContext(TracimContext):
            @property
            def current_user(self):
                return current_user

            @property
            def current_workspace(self):
                return current_workspace

        assert ContentTypeCreationChecker(FakeContentTypeList(),
                                          content_type_slug='test').check(
                                              FakeTracimContext())
Beispiel #13
0
class WorkspaceMemberSchema(marshmallow.Schema):
    role = marshmallow.fields.String(
        example='contributor',
        validate=OneOf(UserRoleInWorkspace.get_all_role_slug()))
    user_id = marshmallow.fields.Int(
        example=3,
        validate=Range(min=1, error="Value must be greater than 0"),
    )
    workspace_id = marshmallow.fields.Int(
        example=4,
        validate=Range(min=1, error="Value must be greater than 0"),
    )
    user = marshmallow.fields.Nested(UserDigestSchema())
    workspace = marshmallow.fields.Nested(
        WorkspaceDigestSchema(exclude=('sidebar_entries', )))
    is_active = marshmallow.fields.Bool()
    do_notify = marshmallow.fields.Bool(
        description='has user enabled notification for this workspace',
        example=True,
    )

    class Meta:
        description = 'Workspace Member information'
Beispiel #14
0
class WorkspaceMemberInviteSchema(marshmallow.Schema):
    role = marshmallow.fields.String(
        example='contributor',
        validate=OneOf(UserRoleInWorkspace.get_all_role_slug()),
        required=True)
    user_id = marshmallow.fields.Int(
        example=5,
        default=None,
        allow_none=True,
    )
    user_email = marshmallow.fields.Email(
        example='*****@*****.**',
        default=None,
        allow_none=True,
    )
    user_public_name = marshmallow.fields.String(
        example='John',
        default=None,
        allow_none=True,
    )

    @post_load
    def make_role(self, data: typing.Dict[str, typing.Any]) -> object:
        return WorkspaceMemberInvitation(**data)
Beispiel #15
0
    def test__unit__RoleChecker__ok__nominal_case(self):

        current_user = User(user_id=2, email='*****@*****.**')
        current_user.groups.append(
            Group(group_id=2, group_name=Group.TIM_MANAGER_GROUPNAME))
        current_workspace = Workspace(workspace_id=3)
        role = UserRoleInWorkspace(user_id=2, workspace_id=3, role=5)
        self.session.add(current_user)
        self.session.add(current_workspace)
        self.session.add(role)
        self.session.flush()
        transaction.commit()

        class FakeTracimContext(TracimContext):
            @property
            def current_user(self):
                return current_user

            @property
            def current_workspace(self):
                return current_workspace

        assert RoleChecker(1).check(FakeTracimContext())
        assert RoleChecker(2).check(FakeTracimContext())
Beispiel #16
0
positive_int_validator = Range(min=0, error="Value must be positive or 0")

# String
# string matching list of int separated by ','
regex_string_as_list_of_int = Regexp(regex=(re.compile('^(\d+(,\d+)*)?$')))
acp_validator = Length(min=2)
not_empty_string_validator = Length(min=1)
action_description_validator = OneOf(ActionDescription.allowed_values())
content_global_status_validator = OneOf(
    [status.value for status in GlobalStatus])
content_status_validator = OneOf(content_status_list.get_all_slugs_values())
user_profile_validator = OneOf(Profile._NAME)
user_timezone_validator = Length(max=User.MAX_TIMEZONE_LENGTH)
user_email_validator = Length(min=User.MIN_EMAIL_LENGTH,
                              max=User.MAX_EMAIL_LENGTH)
user_password_validator = Length(min=User.MIN_PASSWORD_LENGTH,
                                 max=User.MAX_PASSWORD_LENGTH)
user_public_name_validator = Length(min=User.MIN_PUBLIC_NAME_LENGTH,
                                    max=User.MAX_PUBLIC_NAME_LENGTH)
user_lang_validator = Length(min=User.MIN_LANG_LENGTH,
                             max=User.MAX_LANG_LENGTH)
user_role_validator = OneOf(UserRoleInWorkspace.get_all_role_slug())

# Dynamic validator #
all_content_types_validator = OneOf(choices=[])


def update_validators():
    all_content_types_validator.choices = content_type_list.endpoint_allowed_types_slug(
    )  # nopep8
Beispiel #17
0
    def _build_context_for_content_update(
            self,
            role: UserRoleInWorkspace,
            content_in_context: ContentInContext,
            parent_in_context: typing.Optional[ContentInContext],
            workspace_in_context: WorkspaceInContext,
            actor: User,
            translator: Translator
    ):

        _ = translator.get_translation
        content = content_in_context.content
        action = content.get_last_action().id

        # default values
        user = role.user
        workspace = role.workspace
        workspace_url = workspace_in_context.frontend_url
        main_title = content.label
        status_label = content.get_status().label
        # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for status_icon_url  # nopep8
        status_icon_url = ''
        role_label = role.role_as_label()
        content_intro = '<span id="content-intro-username">{}</span> did something.'.format(actor.display_name)  # nopep8
        content_text = content.description
        call_to_action_url = content_in_context.frontend_url
        logo_url = get_email_logo_frontend_url(self.config)

        if ActionDescription.COMMENT == action:
            main_title = parent_in_context.label
            content_intro = ''
            call_to_action_url = parent_in_context.frontend_url
        elif ActionDescription.STATUS_UPDATE == action:
            new_status = translator.get_translation(content.get_status().label)
            main_title = content_in_context.label
            content_intro = _('I modified the status of <i>{content}</i>. The new status is <i>{new_status}</i>').format(
                content=content.get_label(),
                new_status=new_status
            )
            content_text = ''
            call_to_action_url = content_in_context.frontend_url
        elif ActionDescription.CREATION == action:
            main_title = content_in_context.label
            content_intro = _('I added an item entitled <i>{content}</i>.').format(content=content.get_label())  # nopep8
            content_text = ''
        elif action in (ActionDescription.REVISION, ActionDescription.EDITION):
            main_title = content_in_context.label
            content_intro = _('I updated <i>{content}</i>.').format(content=content.get_label())  # nopep8

            previous_revision = content.get_previous_revision()
            title_diff = htmldiff(previous_revision.label, content.label)
            content_diff = htmldiff(previous_revision.description, content.description)
            if title_diff or content_diff:
                content_text = str('<p>{diff_intro_text}</p>\n{title_diff}\n{content_diff}').format(  # nopep8
                    diff_intro_text=_('Here is an overview of the changes:'),
                    title_diff=title_diff,
                    content_diff=content_diff
                )
        # if not content_intro and not content_text:
        #     # Skip notification, but it's not normal
        #     logger.error(
        #         self,
        #         'A notification is being sent but no content. '
        #         'Here are some debug informations: [content_id: {cid}]'
        #         '[action: {act}][author: {actor}]'.format(
        #             cid=content.content_id,
        #             act=action,
        #             actor=actor
        #         )
        #     )
        #     raise EmptyNotificationError('Unexpected empty notification')

        # FIXME: remove/readapt assert to debug easily broken case
        assert user
        assert workspace
        assert main_title
        assert status_label
        # assert status_icon_url
        assert role_label
        # assert content_intro
        assert content_text or content_text == content.description
        assert call_to_action_url
        assert logo_url

        return {
            'user': role.user,
            'workspace': role.workspace,
            'workspace_url': workspace_url,
            'main_title': main_title,
            'status_label': status_label,
            'status_icon_url': status_icon_url,
            'role_label': role_label,
            'content_intro': content_intro,
            'content_text': content_text,
            'call_to_action_url': call_to_action_url,
            'logo_url': logo_url,
        }
Beispiel #18
0
not_empty_string_validator = Length(min=1)
action_description_validator = OneOf(ActionDescription.allowed_values())
content_global_status_validator = OneOf([status.value for status in GlobalStatus])
content_status_validator = OneOf(content_status_list.get_all_slugs_values())
user_profile_validator = OneOf(Profile._NAME)
user_timezone_validator = Length(max=User.MAX_TIMEZONE_LENGTH)
user_email_validator = Length(
    min=User.MIN_EMAIL_LENGTH,
    max=User.MAX_EMAIL_LENGTH
)
user_password_validator = Length(
    min=User.MIN_PASSWORD_LENGTH,
    max=User.MAX_PASSWORD_LENGTH
)
user_public_name_validator = Length(
    min=User.MIN_PUBLIC_NAME_LENGTH,
    max=User.MAX_PUBLIC_NAME_LENGTH
)
user_lang_validator = Length(
    min=User.MIN_LANG_LENGTH,
    max=User.MAX_LANG_LENGTH
)
user_role_validator = OneOf(UserRoleInWorkspace.get_all_role_slug())

# Dynamic validator #
all_content_types_validator = OneOf(choices=[])


def update_validators():
    all_content_types_validator.choices = content_type_list.endpoint_allowed_types_slug()  # nopep8
Beispiel #19
0
    def _build_context_for_content_update(
            self,
            role: UserRoleInWorkspace,
            content_in_context: ContentInContext,
            parent_in_context: typing.Optional[ContentInContext],
            workspace_in_context: WorkspaceInContext,
            actor: User,
            translator: Translator
    ):

        _ = translator.get_translation
        content = content_in_context.content
        action = content.get_last_action().id

        # default values
        user = role.user
        workspace = role.workspace
        workspace_url = workspace_in_context.frontend_url
        main_title = content.label
        status_label = content.get_status().label
        # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for status_icon_url  # nopep8
        status_icon_url = ''
        role_label = role.role_as_label()
        content_intro = '<span id="content-intro-username">{}</span> did something.'.format(actor.display_name)  # nopep8
        content_text = content.description
        call_to_action_text = 'See more'
        call_to_action_url = content_in_context.frontend_url
        logo_url = get_email_logo_frontend_url(self.config)

        if ActionDescription.CREATION == action:
            call_to_action_text = _('View online')
            content_intro = _('<span id="content-intro-username">{}</span> create a content:').format(actor.display_name)  # nopep8

            if content_type_list.Thread.slug == content.type:
                if content.get_last_comment_from(actor):
                    content_text = content.get_last_comment_from(actor).description  # nopep8

                call_to_action_text = _('Answer')
                content_intro = _('<span id="content-intro-username">{}</span> started a thread entitled:').format(actor.display_name)
                content_text = '<p id="content-body-intro">{}</p>'.format(content.label) + content_text  # nopep8

            elif content_type_list.File.slug == content.type:
                content_intro = _('<span id="content-intro-username">{}</span> added a file entitled:').format(actor.display_name)
                if content.description:
                    content_text = content.description
                else:
                    content_text = '<span id="content-body-only-title">{}</span>'.format(content.label)

            elif content_type_list.Page.slug == content.type:
                content_intro = _('<span id="content-intro-username">{}</span> added a page entitled:').format(actor.display_name)
                content_text = '<span id="content-body-only-title">{}</span>'.format(content.label)

        elif ActionDescription.REVISION == action:
            content_text = content.description
            call_to_action_text = _('View online')

            if content_type_list.File.slug == content.type:
                content_intro = _('<span id="content-intro-username">{}</span> uploaded a new revision.').format(actor.display_name)
                content_text = content.description

        elif ActionDescription.EDITION == action:
            call_to_action_text = _('View online')

            if content_type_list.File.slug == content.type:
                content_intro = _('<span id="content-intro-username">{}</span> updated the file description.').format(actor.display_name)
                content_text = '<p id="content-body-intro">{}</p>'.format(content.get_label()) + content.description  # nopep8

            elif content_type_list.Thread.slug == content.type:
                content_intro = _('<span id="content-intro-username">{}</span> updated the thread description.').format(actor.display_name)
                previous_revision = content.get_previous_revision()
                title_diff = ''
                if previous_revision.label != content.label:
                    title_diff = htmldiff(previous_revision.label, content.label)
                content_text = str('<p id="content-body-intro">{}</p> {text} {title_diff} {content_diff}').format(
                    text=_('Here is an overview of the changes:'),
                    title_diff=title_diff,
                    content_diff=htmldiff(previous_revision.description, content.description)
                )
            elif content_type_list.Page.slug == content.type:
                content_intro = _('<span id="content-intro-username">{}</span> updated this page.').format(actor.display_name)
                previous_revision = content.get_previous_revision()
                title_diff = ''
                if previous_revision.label != content.label:
                    title_diff = htmldiff(previous_revision.label, content.label)  # nopep8
                content_text = str('<p id="content-body-intro">{}</p> {text}</p> {title_diff} {content_diff}').format(  # nopep8
                    actor.display_name,
                    text=_('Here is an overview of the changes:'),
                    title_diff=title_diff,
                    content_diff=htmldiff(previous_revision.description, content.description)
                )

        elif ActionDescription.STATUS_UPDATE == action:
            intro_user_msg = _(
                '<span id="content-intro-username">{}</span> '
                'updated the following status:'
            )
            intro_body_msg = '<p id="content-body-intro">{}: {}</p>'

            call_to_action_text = _('View online')
            content_intro = intro_user_msg.format(actor.display_name)
            content_text = intro_body_msg.format(
                content.get_label(),
                content.get_status().label,
            )

        elif ActionDescription.COMMENT == action:
            call_to_action_text = _('Answer')
            main_title = parent_in_context.label
            content_intro = _('<span id="content-intro-username">{}</span> added a comment:').format(actor.display_name)  # nopep8
            call_to_action_url = parent_in_context.frontend_url

        if not content_intro and not content_text:
            # Skip notification, but it's not normal
            logger.error(
                self,
                'A notification is being sent but no content. '
                'Here are some debug informations: [content_id: {cid}]'
                '[action: {act}][author: {actor}]'.format(
                    cid=content.content_id,
                    act=action,
                    actor=actor
                )
            )
            raise EmptyNotificationError('Unexpected empty notification')

        # FIXME: remove/readapt assert to debug easily broken case
        assert user
        assert workspace
        assert main_title
        assert status_label
        # assert status_icon_url
        assert role_label
        assert content_intro
        assert content_text or content_text == content.description
        assert call_to_action_text
        assert call_to_action_url
        assert logo_url

        return {
            'user': role.user,
            'workspace': role.workspace,
            'workspace_url': workspace_url,
            'main_title': main_title,
            'status_label': status_label,
            'status_icon_url': status_icon_url,
            'role_label': role_label,
            'content_intro': content_intro,
            'content_text': content_text,
            'call_to_action_text': call_to_action_text,
            'call_to_action_url': call_to_action_url,
            'logo_url': logo_url,
        }