Ejemplo n.º 1
0
class ThreadController(Controller):
    @hapic.with_api_doc(tags=[SWAGGER_TAG__CONTENT_THREAD_ENDPOINTS])
    @check_right(is_reader)
    @check_right(is_thread_content)
    @hapic.input_path(WorkspaceAndContentIdPathSchema())
    @hapic.output_body(TextBasedContentSchema())
    def get_thread(self,
                   context,
                   request: TracimRequest,
                   hapic_data=None) -> ContentInContext:
        """
        Get thread content
        """
        app_config = request.registry.settings["CFG"]  # type: CFG
        api = ContentApi(
            show_archived=True,
            show_deleted=True,
            current_user=request.current_user,
            session=request.dbsession,
            config=app_config,
        )
        content = api.get_one(hapic_data.path.content_id,
                              content_type=content_type_list.Any_SLUG)
        return api.get_content_in_context(content)

    @hapic.with_api_doc(tags=[SWAGGER_TAG__CONTENT_THREAD_ENDPOINTS])
    @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
    @hapic.handle_exception(ContentFilenameAlreadyUsedInFolder,
                            HTTPStatus.BAD_REQUEST)
    @check_right(is_contributor)
    @check_right(is_thread_content)
    @hapic.input_path(WorkspaceAndContentIdPathSchema())
    @hapic.input_body(TextBasedContentModifySchema())
    @hapic.output_body(TextBasedContentSchema())
    def update_thread(self,
                      context,
                      request: TracimRequest,
                      hapic_data=None) -> ContentInContext:
        """
        update thread
        """
        app_config = request.registry.settings["CFG"]  # type: CFG
        api = ContentApi(
            show_archived=True,
            show_deleted=True,
            current_user=request.current_user,
            session=request.dbsession,
            config=app_config,
        )
        content = api.get_one(hapic_data.path.content_id,
                              content_type=content_type_list.Any_SLUG)
        with new_revision(session=request.dbsession,
                          tm=transaction.manager,
                          content=content):
            api.update_content(
                item=content,
                new_label=hapic_data.body.label,
                new_content=hapic_data.body.raw_content,
            )
            api.save(content)
        return api.get_content_in_context(content)

    @hapic.with_api_doc(tags=[SWAGGER_TAG__CONTENT_THREAD_ENDPOINTS])
    @check_right(is_reader)
    @check_right(is_thread_content)
    @hapic.input_path(WorkspaceAndContentIdPathSchema())
    @hapic.output_body(TextBasedRevisionSchema(many=True))
    def get_thread_revisions(
            self,
            context,
            request: TracimRequest,
            hapic_data=None) -> typing.List[RevisionInContext]:
        """
        get thread revisions
        """
        app_config = request.registry.settings["CFG"]  # type: CFG
        api = ContentApi(
            show_archived=True,
            show_deleted=True,
            current_user=request.current_user,
            session=request.dbsession,
            config=app_config,
        )
        content = api.get_one(hapic_data.path.content_id,
                              content_type=content_type_list.Any_SLUG)
        revisions = content.revisions
        return [
            api.get_revision_in_context(revision) for revision in revisions
        ]

    @hapic.with_api_doc(tags=[SWAGGER_TAG__CONTENT_THREAD_ENDPOINTS])
    @check_right(is_contributor)
    @check_right(is_thread_content)
    @hapic.input_path(WorkspaceAndContentIdPathSchema())
    @hapic.input_body(SetContentStatusSchema())
    @hapic.output_body(NoContentSchema(),
                       default_http_code=HTTPStatus.NO_CONTENT)
    @hapic.handle_exception(ContentStatusException, HTTPStatus.BAD_REQUEST)
    def set_thread_status(self,
                          context,
                          request: TracimRequest,
                          hapic_data=None) -> None:
        """
        set thread status
        """
        app_config = request.registry.settings["CFG"]  # type: CFG
        api = ContentApi(
            show_archived=True,
            show_deleted=True,
            current_user=request.current_user,
            session=request.dbsession,
            config=app_config,
        )
        content = api.get_one(hapic_data.path.content_id,
                              content_type=content_type_list.Any_SLUG)
        if content.status == request.json_body.get("status"):
            raise ContentStatusException(
                "Content id {} already have status {}".format(
                    content.content_id, content.status))
        with new_revision(session=request.dbsession,
                          tm=transaction.manager,
                          content=content):
            api.set_status(content, hapic_data.body.status)
            api.save(content)
        return

    def bind(self, configurator: Configurator) -> None:
        # Get thread
        configurator.add_route(
            "thread",
            "/workspaces/{workspace_id}/threads/{content_id}",
            request_method="GET")
        configurator.add_view(self.get_thread, route_name="thread")

        # update thread
        configurator.add_route(
            "update_thread",
            "/workspaces/{workspace_id}/threads/{content_id}",
            request_method="PUT")
        configurator.add_view(self.update_thread, route_name="update_thread")

        # get thread revisions
        configurator.add_route(
            "thread_revisions",
            "/workspaces/{workspace_id}/threads/{content_id}/revisions",
            request_method="GET",
        )
        configurator.add_view(self.get_thread_revisions,
                              route_name="thread_revisions")

        # get thread revisions
        configurator.add_route(
            "set_thread_status",
            "/workspaces/{workspace_id}/threads/{content_id}/status",
            request_method="PUT",
        )
        configurator.add_view(self.set_thread_status,
                              route_name="set_thread_status")
Ejemplo n.º 2
0
class FolderController(Controller):

    @hapic.with_api_doc(tags=[SWAGGER_TAG__CONTENT_FOLDER_ENDPOINTS])
    @require_workspace_role(UserRoleInWorkspace.READER)
    @require_content_types([FOLDER_TYPE])
    @hapic.input_path(WorkspaceAndContentIdPathSchema())
    @hapic.output_body(TextBasedContentSchema())
    def get_folder(self, context, request: TracimRequest, hapic_data=None) -> ContentInContext:  # nopep8
        """
        Get folder info
        """
        app_config = request.registry.settings['CFG']
        api = ContentApi(
            show_archived=True,
            show_deleted=True,
            current_user=request.current_user,
            session=request.dbsession,
            config=app_config,
        )
        content = api.get_one(
            hapic_data.path.content_id,
            content_type=content_type_list.Any_SLUG
        )
        return api.get_content_in_context(content)

    @hapic.with_api_doc(tags=[SWAGGER_TAG__CONTENT_FOLDER_ENDPOINTS])
    @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
    @hapic.handle_exception(ContentFilenameAlreadyUsedInFolder, HTTPStatus.BAD_REQUEST)
    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
    @require_content_types([FOLDER_TYPE])
    @hapic.input_path(WorkspaceAndContentIdPathSchema())
    @hapic.input_body(FolderContentModifySchema())
    @hapic.output_body(TextBasedContentSchema())
    def update_folder(self, context, request: TracimRequest, hapic_data=None) -> ContentInContext:  # nopep8
        """
        update folder
        """
        app_config = request.registry.settings['CFG']
        api = ContentApi(
            show_archived=True,
            show_deleted=True,
            current_user=request.current_user,
            session=request.dbsession,
            config=app_config,
        )
        content = api.get_one(
            hapic_data.path.content_id,
            content_type=content_type_list.Any_SLUG
        )
        with new_revision(
                session=request.dbsession,
                tm=transaction.manager,
                content=content
        ):
            api.update_content(
                item=content,
                new_label=hapic_data.body.label,
                new_content=hapic_data.body.raw_content,

            )
            api.set_allowed_content(
                content=content,
                allowed_content_type_slug_list=hapic_data.body.sub_content_types  # nopep8
            )
            api.save(content)
        return api.get_content_in_context(content)

    @hapic.with_api_doc(tags=[SWAGGER_TAG__CONTENT_FOLDER_ENDPOINTS])
    @require_workspace_role(UserRoleInWorkspace.READER)
    @require_content_types([FOLDER_TYPE])
    @hapic.input_path(WorkspaceAndContentIdPathSchema())
    @hapic.output_body(TextBasedRevisionSchema(many=True))
    def get_folder_revisions(
            self,
            context,
            request: TracimRequest,
            hapic_data=None
    ) -> typing.List[RevisionInContext]:
        """
        get folder revisions
        """
        app_config = request.registry.settings['CFG']
        api = ContentApi(
            show_archived=True,
            show_deleted=True,
            current_user=request.current_user,
            session=request.dbsession,
            config=app_config,
        )
        content = api.get_one(
            hapic_data.path.content_id,
            content_type=content_type_list.Any_SLUG
        )
        revisions = content.revisions
        return [
            api.get_revision_in_context(revision)
            for revision in revisions
        ]

    @hapic.with_api_doc(tags=[SWAGGER_TAG__CONTENT_FOLDER_ENDPOINTS])
    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
    @require_content_types([FOLDER_TYPE])
    @hapic.input_path(WorkspaceAndContentIdPathSchema())
    @hapic.input_body(SetContentStatusSchema())
    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
    def set_folder_status(self, context, request: TracimRequest, hapic_data=None) -> None:  # nopep8
        """
        set folder status
        """
        app_config = request.registry.settings['CFG']
        api = ContentApi(
            show_archived=True,
            show_deleted=True,
            current_user=request.current_user,
            session=request.dbsession,
            config=app_config,
        )
        content = api.get_one(
            hapic_data.path.content_id,
            content_type=content_type_list.Any_SLUG
        )
        with new_revision(
                session=request.dbsession,
                tm=transaction.manager,
                content=content
        ):
            api.set_status(
                content,
                hapic_data.body.status,
            )
            api.save(content)
        return

    def bind(self, configurator: Configurator) -> None:
        # Get folder
        configurator.add_route(
            'folder',
            '/workspaces/{workspace_id}/folders/{content_id}',
            request_method='GET'
        )
        configurator.add_view(self.get_folder, route_name='folder')  # nopep8

        # update folder
        configurator.add_route(
            'update_folder',
            '/workspaces/{workspace_id}/folders/{content_id}',
            request_method='PUT'
        )  # nopep8
        configurator.add_view(self.update_folder, route_name='update_folder')  # nopep8

        # get folder revisions
        configurator.add_route(
            'folder_revisions',
            '/workspaces/{workspace_id}/folders/{content_id}/revisions',  # nopep8
            request_method='GET'
        )
        configurator.add_view(self.get_folder_revisions, route_name='folder_revisions')  # nopep8

        # get folder revisions
        configurator.add_route(
            'set_folder_status',
            '/workspaces/{workspace_id}/folders/{content_id}/status',  # nopep8
            request_method='PUT'
        )
        configurator.add_view(self.set_folder_status, route_name='set_folder_status')  # nopep8
Ejemplo n.º 3
0
class EventApi:
    """Api to query event & messages"""

    user_schema = UserSchema()
    workspace_schema = WorkspaceSchema()
    content_schemas = {
        COMMENT_TYPE: CommentSchema(),
        HTML_DOCUMENTS_TYPE: TextBasedContentSchema(),
        FILE_TYPE: FileContentSchema(),
        FOLDER_TYPE: TextBasedContentSchema(),
        THREAD_TYPE: TextBasedContentSchema(),
    }
    event_schema = EventSchema()
    workspace_user_role_schema = WorkspaceMemberDigestSchema()
    workspace_subscription_schema = WorkspaceSubscriptionSchema()

    def __init__(self, current_user: Optional[User], session: TracimSession,
                 config: CFG) -> None:
        self._current_user = current_user
        self._session = session
        self._config = config

    def _filter_event_types(self, query: Query, event_types: Optional[
        List[EventTypeDatabaseParameters]], exclude: bool) -> Query:
        if event_types:
            event_type_filters = []
            for event_type in event_types:
                if event_type.subtype:
                    event_type_filter = and_(
                        Event.entity_type == event_type.entity,
                        Event.operation == event_type.operation,
                        Event.entity_subtype == event_type.subtype,
                    )
                else:
                    event_type_filter = and_(
                        Event.entity_type == event_type.entity,
                        Event.operation == event_type.operation,
                    )

                event_type_filters.append(event_type_filter)

            if len(event_type_filters) > 1:
                f = or_(*event_type_filters)
            else:
                f = event_type_filters[0]

            if exclude:
                f = not_(f)

            return query.filter(f)

        return query

    def _base_query(
        self,
        read_status: ReadStatus = ReadStatus.ALL,
        event_id: Optional[int] = None,
        user_id: Optional[int] = None,
        include_event_types: List[EventTypeDatabaseParameters] = None,
        exclude_event_types: List[EventTypeDatabaseParameters] = None,
        exclude_author_ids: Optional[List[int]] = None,
        after_event_id: int = 0,
    ) -> Query:
        query = self._session.query(Message).join(Event)
        if event_id:
            query = query.filter(Message.event_id == event_id)
        if user_id:
            query = query.filter(Message.receiver_id == user_id)
        if read_status == ReadStatus.READ:
            query = query.filter(Message.read != null())
        elif read_status == ReadStatus.UNREAD:
            query = query.filter(Message.read == null())
        else:
            # ALL doesn't need any filtering and is the only other handled case
            assert read_status == ReadStatus.ALL

        query = self._filter_event_types(query, include_event_types, False)
        query = self._filter_event_types(query, exclude_event_types, True)

        if exclude_event_types:
            event_type_filters = []
            for event_type in exclude_event_types:
                if event_type.subtype:
                    event_type_filter = or_(
                        Event.entity_type != event_type.entity,
                        Event.operation != event_type.operation,
                        Event.entity_subtype != event_type.subtype,
                    )
                else:
                    event_type_filter = or_(
                        Event.entity_type != event_type.entity,
                        Event.operation != event_type.operation,
                    )

                event_type_filters.append(event_type_filter)

            if len(event_type_filters) > 1:
                query = query.filter(and_(*event_type_filters))
            else:
                query = query.filter(event_type_filters[0])

        if exclude_author_ids:
            for author_id in exclude_author_ids:
                # RJ & SG - 2020-09-11 - HACK
                # We wanted to use Event.fields["author"] == JSON.NULL instead of this
                # soup involving a cast and a get out of my way text("'null'") to
                # know whether a JSON field is null. However, this does not work on
                # PostgreSQL. See https://github.com/sqlalchemy/sqlalchemy/issues/5575

                query = query.filter(
                    or_(
                        cast(Event.fields["author"], String) == text("'null'"),
                        Event.fields["author"]["user_id"].as_integer() !=
                        author_id,
                    ))

        if after_event_id:
            query = query.filter(Message.event_id > after_event_id)

        return query

    def get_one_message(self, event_id: int, user_id: int) -> Message:
        try:
            return self._base_query(event_id=event_id, user_id=user_id).one()
        except NoResultFound as exc:
            raise MessageDoesNotExist(
                'Message for user {} with event id "{}" not found in database'.
                format(user_id, event_id)) from exc

    def mark_user_message_as_read(self, event_id: int,
                                  user_id: int) -> Message:
        message = self.get_one_message(event_id, user_id)
        message.read = datetime.utcnow()
        self._session.add(message)
        self._session.flush()
        return message

    def mark_user_message_as_unread(self, event_id: int,
                                    user_id: int) -> Message:
        message = self.get_one_message(event_id, user_id)
        message.read = None
        self._session.add(message)
        self._session.flush()
        return message

    def mark_user_messages_as_read(self, user_id: int) -> List[Message]:
        unread_messages = self._base_query(read_status=ReadStatus.UNREAD,
                                           user_id=user_id).all()
        for message in unread_messages:
            message.read = datetime.utcnow()
            self._session.add(message)
        self._session.flush()
        return unread_messages

    def get_messages_for_user(self,
                              user_id: int,
                              after_event_id: int = 0) -> List[Message]:
        query = self._base_query(
            user_id=user_id,
            after_event_id=after_event_id,
        )
        return query.all()

    def get_paginated_messages_for_user(
        self,
        user_id: int,
        read_status: ReadStatus,
        exclude_author_ids: List[int] = None,
        include_event_types: List[EventTypeDatabaseParameters] = None,
        exclude_event_types: List[EventTypeDatabaseParameters] = None,
        count: Optional[int] = DEFAULT_NB_ITEM_PAGINATION,
        page_token: Optional[int] = None,
    ) -> Page:
        query = self._base_query(
            user_id=user_id,
            read_status=read_status,
            include_event_types=include_event_types,
            exclude_event_types=exclude_event_types,
            exclude_author_ids=exclude_author_ids,
        ).order_by(Message.event_id.desc())
        return get_page(query, per_page=count, page=page_token or False)

    def get_messages_count(
        self,
        user_id: int,
        read_status: ReadStatus,
        include_event_types: List[EventTypeDatabaseParameters] = None,
        exclude_event_types: List[EventTypeDatabaseParameters] = None,
        exclude_author_ids: List[int] = None,
    ) -> int:
        return self._base_query(
            user_id=user_id,
            include_event_types=include_event_types,
            exclude_event_types=exclude_event_types,
            read_status=read_status,
            exclude_author_ids=exclude_author_ids,
        ).count()

    def create_event(
        self,
        entity_type: EntityType,
        operation: OperationType,
        additional_fields: Dict[str, JsonDict],
        context: TracimContext,
        entity_subtype: Optional[str] = None,
    ) -> Event:
        current_user = context.safe_current_user()
        user_api = UserApi(
            current_user=current_user,
            session=context.dbsession,
            config=self._config,
            show_deleted=True,
        )
        if current_user:
            author = self.user_schema.dump(
                user_api.get_user_with_context(current_user)).data
        else:
            author = None
        fields = {
            Event.AUTHOR_FIELD: author,
            Event.CLIENT_TOKEN_FIELD: context.client_token,
        }
        fields.update(additional_fields)
        event = Event(
            entity_type=entity_type,
            operation=operation,
            entity_subtype=entity_subtype,
            fields=fields,
        )
        context.dbsession.add(event)
        context.pending_events.append(event)
        return event

    @classmethod
    def get_content_schema_for_type(cls, content_type: str) -> ContentSchema:
        try:
            return cls.content_schemas[content_type]
        except KeyError:
            logger.error(
                cls,
                ("Cannot dump proper content for content-type '{}' in generated event "
                 "as it is unknown, falling back to generic content schema"
                 ).format(content_type),
            )
            return ContentSchema()
Ejemplo n.º 4
0
class HTMLDocumentController(Controller):
    @hapic.with_api_doc(tags=[SWAGGER_TAG__CONTENT_HTML_DOCUMENT_ENDPOINTS])
    @check_right(is_reader)
    @check_right(is_html_document_content)
    @hapic.input_path(WorkspaceAndContentIdPathSchema())
    @hapic.output_body(TextBasedContentSchema())
    def get_html_document(
        self, context, request: TracimRequest, hapic_data=None
    ) -> ContentInContext:
        """
        Get html document content
        """
        app_config = request.registry.settings["CFG"]  # type: CFG
        api = ContentApi(
            show_archived=True,
            show_deleted=True,
            current_user=request.current_user,
            session=request.dbsession,
            config=app_config,
        )
        content = api.get_one(hapic_data.path.content_id, content_type=content_type_list.Any_SLUG)
        return api.get_content_in_context(content)

    @hapic.with_api_doc(tags=[SWAGGER_TAG__CONTENT_HTML_DOCUMENT_ENDPOINTS])
    @check_right(is_reader)
    @check_right(is_html_document_content)
    @hapic.input_query(FileQuerySchema())
    @hapic.input_path(FilePathSchema())
    @hapic.output_file([])
    def get_html_document_preview(
        self, context, request: TracimRequest, hapic_data=None
    ) -> HapicFile:
        """
           Download preview of html document
           Good pratice for filename is filename is `{label}{file_extension}` or `{filename}`.
           Default filename value is 'raw' (without file extension) or nothing.
           """
        app_config = request.registry.settings["CFG"]  # type: CFG
        api = ContentApi(
            show_archived=True,
            show_deleted=True,
            current_user=request.current_user,
            session=request.dbsession,
            config=app_config,
        )
        content = api.get_one(hapic_data.path.content_id, content_type=content_type_list.Any_SLUG)
        file = BytesIO()
        byte_size = file.write(content.description.encode("utf-8"))
        file.seek(0)
        filename = hapic_data.path.filename
        # INFO - G.M - 2019-08-08 - use given filename in all case but none or
        # "raw", where filename returned will be original file one.
        if not filename or filename == "raw":
            filename = content.file_name
        return HapicFile(
            file_object=file,
            mimetype=CONTENT_TYPE_TEXT_HTML,
            filename=filename,
            as_attachment=hapic_data.query.force_download,
            content_length=byte_size,
            last_modified=content.updated,
        )

    @hapic.with_api_doc(tags=[SWAGGER_TAG__CONTENT_HTML_DOCUMENT_ENDPOINTS])
    @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
    @hapic.handle_exception(ContentFilenameAlreadyUsedInFolder, HTTPStatus.BAD_REQUEST)
    @hapic.handle_exception(UserNotMemberOfWorkspace, HTTPStatus.BAD_REQUEST)
    @check_right(is_contributor)
    @check_right(is_html_document_content)
    @hapic.input_path(WorkspaceAndContentIdPathSchema())
    @hapic.input_body(TextBasedContentModifySchema())
    @hapic.output_body(TextBasedContentSchema())
    def update_html_document(
        self, context, request: TracimRequest, hapic_data=None
    ) -> ContentInContext:
        """
        update_html_document
        """
        app_config = request.registry.settings["CFG"]  # type: CFG
        api = ContentApi(
            show_archived=True,
            show_deleted=True,
            current_user=request.current_user,
            session=request.dbsession,
            config=app_config,
        )
        content = api.get_one(hapic_data.path.content_id, content_type=content_type_list.Any_SLUG)
        with new_revision(session=request.dbsession, tm=transaction.manager, content=content):
            api.update_content(
                item=content,
                new_label=hapic_data.body.label,
                new_content=hapic_data.body.raw_content,
            )
            api.save(content)
            api.execute_update_content_actions(content)
        return api.get_content_in_context(content)

    @hapic.with_api_doc(tags=[SWAGGER_TAG__CONTENT_HTML_DOCUMENT_ENDPOINTS])
    @check_right(is_reader)
    @check_right(is_html_document_content)
    @hapic.input_path(WorkspaceAndContentIdPathSchema())
    @hapic.output_body(TextBasedRevisionSchema(many=True))
    def get_html_document_revisions(
        self, context, request: TracimRequest, hapic_data=None
    ) -> typing.List[RevisionInContext]:
        """
        get html_document revisions
        """
        app_config = request.registry.settings["CFG"]  # type: CFG
        api = ContentApi(
            show_archived=True,
            show_deleted=True,
            current_user=request.current_user,
            session=request.dbsession,
            config=app_config,
        )
        content = api.get_one(hapic_data.path.content_id, content_type=content_type_list.Any_SLUG)
        revisions = content.revisions
        return [api.get_revision_in_context(revision) for revision in revisions]

    @hapic.with_api_doc(tags=[SWAGGER_TAG__CONTENT_HTML_DOCUMENT_ENDPOINTS])
    @check_right(is_contributor)
    @check_right(is_html_document_content)
    @hapic.input_path(WorkspaceAndContentIdPathSchema())
    @hapic.input_body(SetContentStatusSchema())
    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)
    @hapic.handle_exception(ContentStatusException, HTTPStatus.BAD_REQUEST)
    def set_html_document_status(self, context, request: TracimRequest, hapic_data=None) -> None:
        """
        set html_document status
        """
        app_config = request.registry.settings["CFG"]  # type: CFG
        api = ContentApi(
            show_archived=True,
            show_deleted=True,
            current_user=request.current_user,
            session=request.dbsession,
            config=app_config,
        )
        content = api.get_one(hapic_data.path.content_id, content_type=content_type_list.Any_SLUG)
        if content.status == request.json_body.get("status"):
            raise ContentStatusException(
                "Content id {} already have status {}".format(content.content_id, content.status)
            )
        with new_revision(session=request.dbsession, tm=transaction.manager, content=content):
            api.set_status(content, hapic_data.body.status)
            api.save(content)
            api.execute_update_content_actions(content)
        return

    def bind(self, configurator: Configurator) -> None:
        # Get html-document
        configurator.add_route(
            "html_document",
            "/workspaces/{workspace_id}/html-documents/{content_id}",
            request_method="GET",
        )
        configurator.add_view(self.get_html_document, route_name="html_document")

        # get html-document preview
        configurator.add_route(
            "preview_html",
            "/workspaces/{workspace_id}/html-documents/{content_id}/preview/html/{filename:[^/]*}",
            request_method="GET",
        )
        configurator.add_view(self.get_html_document_preview, route_name="preview_html")

        # update html-document
        configurator.add_route(
            "update_html_document",
            "/workspaces/{workspace_id}/html-documents/{content_id}",
            request_method="PUT",
        )
        configurator.add_view(self.update_html_document, route_name="update_html_document")

        # get html document revisions
        configurator.add_route(
            "html_document_revisions",
            "/workspaces/{workspace_id}/html-documents/{content_id}/revisions",
            request_method="GET",
        )
        configurator.add_view(
            self.get_html_document_revisions, route_name="html_document_revisions"
        )

        # get html document revisions
        configurator.add_route(
            "set_html_document_status",
            "/workspaces/{workspace_id}/html-documents/{content_id}/status",
            request_method="PUT",
        )
        configurator.add_view(self.set_html_document_status, route_name="set_html_document_status")