Example #1
0
def new_revision(
        session: Session,
        tm: TransactionManager,
        content: Content,
        force_create_new_revision: bool=False,
) -> Content:
    """
    Prepare context to update a Content. It will add a new updatable revision
    to the content.
    :param session: Database _session
    :param tm: TransactionManager
    :param content: Content instance to update
    :param force_create_new_revision: Decide if new_rev should or should not
    be forced.
    :return:
    """
    with session.no_autoflush:
        try:
            if force_create_new_revision \
                    or inspect(content.revision).has_identity:
                content.new_revision()
            RevisionsIntegrity.add_to_updatable(content.revision)
            yield content
        except SameValueError or ValueError as e:
            # INFO - 20-03-2018 - renew transaction when error happened
            # This avoid bad _session data like new "temporary" revision
            # to be add when problem happen.
            tm.abort()
            tm.begin()
            raise e
        finally:
            RevisionsIntegrity.remove_from_updatable(content.revision)
Example #2
0
    def create(self,
               content_type: str,
               workspace: Workspace,
               parent: Content = None,
               label: str = '',
               do_save=False,
               is_temporary: bool = False) -> Content:
        assert content_type in ContentType.allowed_types()

        if content_type == ContentType.Folder and not label:
            label = self.generate_folder_label(workspace, parent)

        content = Content()
        content.owner = self._user
        content.parent = parent
        content.workspace = workspace
        content.type = content_type
        content.label = label
        content.is_temporary = is_temporary
        content.revision_type = ActionDescription.CREATION

        if content.type in (
                ContentType.Page,
                ContentType.Thread,
        ):
            content.file_extension = '.html'

        if do_save:
            self._session.add(content)
            self.save(content, ActionDescription.CREATION)
        return content
Example #3
0
 def set_status(self, content: Content, new_status: str):
     if new_status in ContentStatus.allowed_values():
         content.status = new_status
         content.revision_type = ActionDescription.STATUS_UPDATE
     else:
         raise ValueError(
             'The given value {} is not allowed'.format(new_status))
Example #4
0
def compare_content_for_sorting_by_type_and_name(content1: Content,
                                                 content2: Content) -> int:
    """
    :param content1:
    :param content2:
    :return:    1 if content1 > content2
                -1 if content1 < content2
                0 if content1 = content2
    """

    if content1.type == content2.type:
        if content1.get_label().lower() > content2.get_label().lower():
            return 1
        elif content1.get_label().lower() < content2.get_label().lower():
            return -1
        return 0
    else:
        # TODO - D.A. - 2014-12-02 - Manage Content Types Dynamically
        content_type_order = [
            ContentType.Folder,
            ContentType.Page,
            ContentType.Thread,
            ContentType.File,
        ]

        content_1_type_index = content_type_order.index(content1.type)
        content_2_type_index = content_type_order.index(content2.type)
        result = content_1_type_index - content_2_type_index

        if result < 0:
            return -1
        elif result > 0:
            return 1
        else:
            return 0
Example #5
0
 def delete(self, content: Content):
     content.owner = self._user
     content.is_deleted = True
     # TODO - G.M - 12-03-2018 - Inspect possible label conflict problem
     # INFO - G.M - 12-03-2018 - Set label name to avoid trouble when
     # un-deleting file.
     content.label = '{label}-{action}-{date}'.format(
         label=content.label,
         action='deleted',
         date=current_date_for_filename())
     content.revision_type = ActionDescription.DELETION
Example #6
0
 def update_content(self,
                    item: Content,
                    new_label: str,
                    new_content: str = None) -> Content:
     if item.label == new_label and item.description == new_content:
         # TODO - G.M - 20-03-2018 - Fix internatization for webdav access.
         # Internatization disabled in libcontent for now.
         raise SameValueError('The content did not changed')
     item.owner = self._user
     item.label = new_label
     item.description = new_content if new_content else item.description  # TODO: convert urls into links
     item.revision_type = ActionDescription.EDITION
     return item
Example #7
0
 def update_file_data(self, item: Content, new_filename: str,
                      new_mimetype: str, new_content: bytes) -> Content:
     if new_mimetype == item.file_mimetype and \
             new_content == item.depot_file.file.read():
         raise SameValueError('The content did not changed')
     item.owner = self._user
     item.file_name = new_filename
     item.file_mimetype = new_mimetype
     item.depot_file = FileIntent(
         new_content,
         new_filename,
         new_mimetype,
     )
     item.revision_type = ActionDescription.REVISION
     return item
Example #8
0
    def move(self,
             item: Content,
             new_parent: Content,
             must_stay_in_same_workspace: bool = True,
             new_workspace: Workspace = None):
        if must_stay_in_same_workspace:
            if new_parent and new_parent.workspace_id != item.workspace_id:
                raise ValueError('the item should stay in the same workspace')

        item.parent = new_parent
        if new_parent:
            item.workspace = new_parent.workspace
        elif new_workspace:
            item.workspace = new_workspace

        item.revision_type = ActionDescription.MOVE
Example #9
0
    def create_comment(self,
                       workspace: Workspace = None,
                       parent: Content = None,
                       content: str = '',
                       do_save=False) -> Content:
        assert parent and parent.type != ContentType.Folder
        item = Content()
        item.owner = self._user
        item.parent = parent
        item.workspace = workspace
        item.type = ContentType.Comment
        item.description = content
        item.label = ''
        item.revision_type = ActionDescription.COMMENT

        if do_save:
            self.save(item, ActionDescription.COMMENT)
        return item
Example #10
0
    def mark_read(self,
                  content: Content,
                  read_datetime: datetime = None,
                  do_flush: bool = True,
                  recursive: bool = True) -> Content:

        assert self._user
        assert content

        # The algorithm is:
        # 1. define the read datetime
        # 2. update all revisions related to current Content
        # 3. do the same for all child revisions
        #    (ie parent_id is content_id of current content)

        if not read_datetime:
            read_datetime = datetime.datetime.now()

        viewed_revisions = self._session.query(ContentRevisionRO) \
            .filter(ContentRevisionRO.content_id==content.content_id).all()

        for revision in viewed_revisions:
            revision.read_by[self._user] = read_datetime

        if recursive:
            # mark read :
            # - all children
            # - parent stuff (if you mark a comment as read,
            #                 then you have seen the parent)
            # - parent comments
            for child in content.get_valid_children():
                self.mark_read(child,
                               read_datetime=read_datetime,
                               do_flush=False)

            if ContentType.Comment == content.type:
                self.mark_read(content.parent,
                               read_datetime=read_datetime,
                               do_flush=False,
                               recursive=False)
                for comment in content.parent.get_comments():
                    if comment != content:
                        self.mark_read(comment,
                                       read_datetime=read_datetime,
                                       do_flush=False,
                                       recursive=False)

        if do_flush:
            self.flush()

        return content
Example #11
0
 def set_allowed_content(self, folder: Content, allowed_content_dict: dict):
     """
     :param folder: the given folder instance
     :param allowed_content_dict: must be something like this:
         dict(
             folder = True
             thread = True,
             file = False,
             page = True
         )
     :return:
     """
     properties = dict(allowed_content=allowed_content_dict)
     folder.properties = properties
Example #12
0
    def _create_content_and_test(self, name, workspace, *args,
                                 **kwargs) -> Content:
        """
        All extra parameters (*args, **kwargs) are for Content init
        :return: Created Content instance
        """
        content = Content(*args, **kwargs)
        content.label = name
        content.workspace = workspace
        self.session.add(content)
        self.session.flush()

        content_api = ContentApi(
            current_user=None,
            session=self.session,
            config=self.app_config,
        )
        eq_(
            1,
            content_api.get_canonical_query().filter(
                Content.label == name).count())
        return content_api.get_canonical_query().filter(
            Content.label == name).one()
Example #13
0
    def copy(
        self,
        item: Content,
        new_parent: Content = None,
        new_label: str = None,
        do_save: bool = True,
        do_notify: bool = True,
    ) -> Content:
        """
        Copy nearly all content, revision included. Children not included, see
        "copy_children" for this.
        :param item: Item to copy
        :param new_parent: new parent of the new copied item
        :param new_label: new label of the new copied item
        :param do_notify: notify copy or not
        :return: Newly copied item
        """
        if (not new_parent
                and not new_label) or (new_parent == item.parent
                                       and new_label == item.label):  # nopep8
            # TODO - G.M - 08-03-2018 - Use something else than value error
            raise ValueError("You can't copy file into itself")
        if new_parent:
            workspace = new_parent.workspace
            parent = new_parent
        else:
            workspace = item.workspace
            parent = item.parent
        label = new_label or item.label

        content = item.copy(parent)
        # INFO - GM - 15-03-2018 - add "copy" revision
        with new_revision(session=self._session,
                          tm=transaction.manager,
                          content=content,
                          force_create_new_revision=True) as rev:
            rev.parent = parent
            rev.workspace = workspace
            rev.label = label
            rev.revision_type = ActionDescription.COPY
            rev.properties['origin'] = {
                'content': item.id,
                'revision': item.last_revision.revision_id,
            }
        if do_save:
            self.save(content, ActionDescription.COPY, do_notify=do_notify)
        return content
Example #14
0
    def mark_unread(self, content: Content, do_flush=True) -> Content:
        assert self._user
        assert content

        revisions = self._session.query(ContentRevisionRO) \
            .filter(ContentRevisionRO.content_id==content.content_id).all()

        for revision in revisions:
            del revision.read_by[self._user]

        for child in content.get_valid_children():
            self.mark_unread(child, do_flush=False)

        if do_flush:
            self.flush()

        return content
Example #15
0
    def save(self,
             content: Content,
             action_description: str = None,
             do_flush=True,
             do_notify=True):
        """
        Save an object, flush the session and set the revision_type property
        :param content:
        :param action_description:
        :return:
        """
        assert action_description is None or action_description in ActionDescription.allowed_values(
        )

        if not action_description:
            # See if the last action has been modified
            if content.revision_type == None or len(
                    get_history(content.revision, 'revision_type')) <= 0:
                # The action has not been modified, so we set it to default edition
                action_description = ActionDescription.EDITION

        if action_description:
            content.revision_type = action_description

        if do_flush:
            # INFO - 2015-09-03 - D.A.
            # There are 2 flush because of the use
            # of triggers for content creation
            #
            # (when creating a content, actually this is an insert of a new
            # revision in content_revisions ; so the mark_read operation need
            # to get full real data from database before to be prepared.

            self._session.add(content)
            self._session.flush()

            # TODO - 2015-09-03 - D.A. - Do not use triggers
            # We should create a new ContentRevisionRO object instead of Content
            # This would help managing view/not viewed status
            self.mark_read(content, do_flush=True)

        if do_notify:
            self.do_notify(content)
Example #16
0
    def _build_email_body_for_content(
            self,
            mako_template_filepath: str,
            role: UserRoleInWorkspace,
            content: Content,
            actor: User
    ) -> str:
        """
        Build an email body and return it as a string
        :param mako_template_filepath: the absolute path to the mako template to be used for email body building
        :param role: the role related to user to whom the email must be sent. The role is required (and not the user only) in order to show in the mail why the user receive the notification
        :param content: the content item related to the notification
        :param actor: the user at the origin of the action / notification (for example the one who wrote a comment
        :param config: the global configuration
        :return: the built email body as string. In case of multipart email, this method must be called one time for text and one time for html
        """
        logger.debug(self, 'Building email content from MAKO template {}'.format(mako_template_filepath))

        main_title = content.label
        content_intro = ''
        content_text = ''
        call_to_action_text = ''

        # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for call_to_action_url  # nopep8
        call_to_action_url =''
        # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for status_icon_url  # nopep8
        status_icon_url = ''
        # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for workspace_url  # nopep8
        workspace_url = ''
        # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for logo_url  # nopep8
        logo_url = ''

        action = content.get_last_action().id
        if ActionDescription.COMMENT == action:
            content_intro = l_('<span id="content-intro-username">{}</span> added a comment:').format(actor.display_name)
            content_text = content.description
            call_to_action_text = l_('Answer')

        elif ActionDescription.CREATION == action:

            # Default values (if not overriden)
            content_text = content.description
            call_to_action_text = l_('View online')

            if ContentType.Thread == content.type:
                call_to_action_text = l_('Answer')
                content_intro = l_('<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.get_last_comment_from(actor).description

            elif ContentType.File == content.type:
                content_intro = l_('<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 ContentType.Page == content.type:
                content_intro = l_('<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 = l_('View online')

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

            elif ContentType.Page == content.type:
                content_intro = l_('<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)
                content_text = str(l_('<p id="content-body-intro">Here is an overview of the changes:</p>'))+ \
                    title_diff + \
                    htmldiff(previous_revision.description, content.description)

            elif ContentType.Thread == content.type:
                content_intro = l_('<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(l_('<p id="content-body-intro">Here is an overview of the changes:</p>'))+ \
                    title_diff + \
                    htmldiff(previous_revision.description, content.description)

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

            if ContentType.File == content.type:
                content_intro = l_('<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

        elif ActionDescription.STATUS_UPDATE == action:
            call_to_action_text = l_('View online')
            intro_user_msg = l_(
                '<span id="content-intro-username">{}</span> '
                'updated the following status:'
            )
            content_intro = intro_user_msg.format(actor.display_name)
            intro_body_msg = '<p id="content-body-intro">{}: {}</p>'
            content_text = intro_body_msg.format(
                content.get_label(),
                content.get_status().label,
            )

        if '' == content_intro and 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 ValueError('Unexpected empty notification')

        context = {
            'user': role.user,
            'workspace': role.workspace,
            'workspace_url': workspace_url,
            'main_title': main_title,
            'status_label': content.get_status().label,
            'status_icon_url': status_icon_url,
            'role_label': role.role_as_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,
        }
        user = role.user
        workspace = role.workspace
        body_content = self._render_template(
            mako_template_filepath=mako_template_filepath,
            context=context,
        )
        return body_content
Example #17
0
def designPage(content: data.Content,
               content_revision: data.ContentRevisionRO) -> str:
    hist = content.get_history(drop_empty_revision=False)
    histHTML = '<table class="table table-striped table-hover">'
    for event in hist:
        if isinstance(event, VirtualEvent):
            date = event.create_readable_date()
            label = _LABELS[event.type.id]

            histHTML += '''
                <tr class="%s">
                    <td class="my-align"><span class="label label-default"><i class="fa %s"></i> %s</span></td>
                    <td>%s</td>
                    <td>%s</td>
                    <td>%s</td>
                </tr>
                ''' % (
                'warning' if event.id == content_revision.revision_id else '',
                event.type.fa_icon,
                label,
                date,
                event.owner.display_name,
                # NOTE: (WABDAV_HIST_DEL_DISABLED) Disabled for beta 1.0
                '<i class="fa fa-caret-left"></i> shown'
                if event.id == content_revision.revision_id else
                ''  # '''<span><a class="revision-link" href="/.history/%s/(%s - %s) %s.html">(View revision)</a></span>''' % (
                # content.label, event.id, event.type.id, event.ref_object.label) if event.type.id in ['revision', 'creation', 'edition'] else '')
            )
    histHTML += '</table>'

    page = '''
<html>
<head>
	<meta charset="utf-8" />
	<title>%s</title>
	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css">
	<style>%s</style>
	<script type="text/javascript" src="/home/arnaud/Documents/css/script.js"></script>
	<script
			  src="https://code.jquery.com/jquery-3.1.0.min.js"
			  integrity="sha256-cCueBR6CsyA4/9szpPfrX3s49M9vUU5BgtiJj06wt/s="
			  crossorigin="anonymous"></script>
</head>
<body>
    <div id="left" class="col-lg-8 col-md-12 col-sm-12 col-xs-12">
        <div class="title page">
            <div class="title-text">
                <i class="fa fa-file-text-o title-icon page"></i>
                <h1>%s</h1>
                <h6>page created on <b>%s</b> by <b>%s</b></h6>
            </div>
            <div class="pull-right">
                <div class="btn-group btn-group-vertical">
                    <!-- NOTE: Not omplemented yet, don't display not working link
                     <a class="btn btn-default">
                         <i class="fa fa-external-link"></i> View in tracim</a>
                     </a>-->
                </div>
            </div>
        </div>
        <div class="content col-xs-12 col-sm-12 col-md-12 col-lg-12">
            %s
        </div>
    </div>
    <div id="right" class="col-lg-4 col-md-12 col-sm-12 col-xs-12">
        <h4>History</h4>
        %s
    </div>
    <script type="text/javascript">
        window.onload = function() {
            file_location = window.location.href
            file_location = file_location.replace(/\/[^/]*$/, '')
            file_location = file_location.replace(/\/.history\/[^/]*$/, '')

            // NOTE: (WABDAV_HIST_DEL_DISABLED) Disabled for beta 1.0
            // $('.revision-link').each(function() {
            //    $(this).attr('href', file_location + $(this).attr('href'))
            // });
        }
    </script>
</body>
</html>
        ''' % (content_revision.label, style, content_revision.label,
               content.created.strftime("%B %d, %Y at %H:%m"),
               content.owner.display_name, content_revision.description,
               histHTML)

    return page
Example #18
0
    def notify_content_update(self, content: Content):

        if content.get_last_action().id not \
                in self.config.EMAIL_NOTIFICATION_NOTIFIED_EVENTS:
            logger.info(
                self,
                'Skip email notification for update of content {}'
                'by user {} (the action is {})'.format(
                    content.content_id,
                    # below: 0 means "no user"
                    self._user.user_id if self._user else 0,
                    content.get_last_action().id
                )
            )
            return

        logger.info(self,
                    'About to email-notify update'
                    'of content {} by user {}'.format(
                        content.content_id,
                        # Below: 0 means "no user"
                        self._user.user_id if self._user else 0
                    )
        )

        if content.type not \
                in self.config.EMAIL_NOTIFICATION_NOTIFIED_CONTENTS:
            logger.info(
                self,
                'Skip email notification for update of content {}'
                'by user {} (the content type is {})'.format(
                    content.type,
                    # below: 0 means "no user"
                    self._user.user_id if self._user else 0,
                    content.get_last_action().id
                )
            )
            return

        logger.info(self,
                    'About to email-notify update'
                    'of content {} by user {}'.format(
                        content.content_id,
                        # Below: 0 means "no user"
                        self._user.user_id if self._user else 0
                    )
        )

        ####
        #
        # INFO - D.A. - 2014-11-05 - Emails are sent through asynchronous jobs.
        # For that reason, we do not give SQLAlchemy objects but ids only
        # (SQLA objects are related to a given thread/session)
        #
        try:
            if self.config.EMAIL_NOTIFICATION_PROCESSING_MODE.lower() == self.config.CST.ASYNC.lower():
                logger.info(self, 'Sending email in ASYNC mode')
                # TODO - D.A - 2014-11-06
                # This feature must be implemented in order to be able to scale to large communities
                raise NotImplementedError('Sending emails through ASYNC mode is not working yet')
            else:
                logger.info(self, 'Sending email in SYNC mode')
                EmailManager(
                    self._smtp_config,
                    self.config,
                    self.session,
                ).notify_content_update(self._user.user_id, content.content_id)
        except TypeError as e:
            logger.error(self, 'Exception catched during email notification: {}'.format(e.__str__()))
Example #19
0
 def test_dummy_notifier__notify_content_update(self):
     c = Content()
     notifier = DummyNotifier(self.app_config, self.session)
     notifier.notify_content_update(c)
Example #20
0
 def unarchive(self, content: Content):
     content.owner = self._user
     content.is_archived = False
     content.revision_type = ActionDescription.UNARCHIVING
Example #21
0
 def undelete(self, content: Content):
     content.owner = self._user
     content.is_deleted = False
     content.revision_type = ActionDescription.UNDELETION
Example #22
0
def designThread(content: data.Content,
                 content_revision: data.ContentRevisionRO, comments) -> str:
    hist = content.get_history(drop_empty_revision=False)

    allT = []
    allT += comments
    allT += hist
    allT.sort(key=lambda x: x.created, reverse=True)

    disc = ''
    participants = {}
    for t in allT:
        if t.type == ContentType.Comment:
            disc += '''
                    <div class="row comment comment-row">
                        <i class="fa fa-comment-o comment-icon"></i>
                            <div class="comment-content">
                            <h5>
                                <span class="comment-author"><b>%s</b> wrote :</span>
                                <div class="pull-right text-right">%s</div>
                            </h5>
                            %s
                        </div>
                    </div>
                    ''' % (t.owner.display_name, create_readable_date(
                t.created), t.description)

            if t.owner.display_name not in participants:
                participants[t.owner.display_name] = [1, t.created]
            else:
                participants[t.owner.display_name][0] += 1
        else:
            if isinstance(t, VirtualEvent) and t.type.id != 'comment':
                label = _LABELS[t.type.id]

                disc += '''
                    <div class="%s row comment comment-row to-hide">
                        <i class="fa %s comment-icon"></i>
                            <div class="comment-content">
                            <h5>
                                <span class="comment-author"><b>%s</b></span>
                                <div class="pull-right text-right">%s</div>
                            </h5>
                            %s %s
                        </div>
                    </div>
                    ''' % (
                    'warning' if t.id == content_revision.revision_id else '',
                    t.type.fa_icon,
                    t.owner.display_name,
                    t.create_readable_date(),
                    label,
                    # NOTE: (WABDAV_HIST_DEL_DISABLED) Disabled for beta 1.0
                    '<i class="fa fa-caret-left"></i> shown'
                    if t.id == content_revision.revision_id else
                    ''  # else '''<span><a class="revision-link" href="/.history/%s/%s-%s">(View revision)</a></span>''' % (
                    # content.label,
                    # t.id,
                    # t.ref_object.label) if t.type.id in ['revision', 'creation', 'edition'] else '')
                )

    thread = '''
<html>
<head>
	<meta charset="utf-8" />
	<title>%s</title>
	<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css">
	<style>%s</style>
	<script type="text/javascript" src="/home/arnaud/Documents/css/script.js"></script>
</head>
<body>
    <div id="left" class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
        <div class="title thread">
            <div class="title-text">
                <i class="fa fa-comments-o title-icon thread"></i>
                <h1>%s</h1>
                <h6>thread created on <b>%s</b> by <b>%s</b></h6>
            </div>
            <div class="pull-right">
                <div class="btn-group btn-group-vertical">
                    <!-- NOTE: Not omplemented yet, don't display not working link
                    <a class="btn btn-default" onclick="hide_elements()">
                       <i id="hideshow" class="fa fa-eye-slash"></i> <span id="hideshowtxt" >Hide history</span></a>
                    </a>-->
                    <a class="btn btn-default">
                        <i class="fa fa-external-link"></i> View in tracim</a>
                    </a>
                </div>
            </div>
        </div>
        <div class="content col-xs-12 col-sm-12 col-md-12 col-lg-12">
            <div class="description">
                <span class="description-text">%s</span>
            </div>
            %s
        </div>
    </div>
    <script type="text/javascript">
        window.onload = function() {
            file_location = window.location.href
            file_location = file_location.replace(/\/[^/]*$/, '')
            file_location = file_location.replace(/\/.history\/[^/]*$/, '')

            // NOTE: (WABDAV_HIST_DEL_DISABLED) Disabled for beta 1.0
            // $('.revision-link').each(function() {
            //     $(this).attr('href', file_location + $(this).attr('href'))
            // });
        }

        function hide_elements() {
            elems = document.getElementsByClassName('to-hide');
            if (elems.length > 0) {
                for(var i = 0; i < elems.length; i++) {
                    $(elems[i]).addClass('to-show')
                    $(elems[i]).hide();
                }
                while (elems.length>0) {
                    $(elems[0]).removeClass('comment-row');
                    $(elems[0]).removeClass('to-hide');
                }
                $('#hideshow').addClass('fa-eye').removeClass('fa-eye-slash');
                $('#hideshowtxt').html('Show history');
            }
            else {
                elems = document.getElementsByClassName('to-show');
                for(var i = 0; i<elems.length; i++) {
                    $(elems[0]).addClass('comment-row');
                    $(elems[i]).addClass('to-hide');
                    $(elems[i]).show();
                }
                while (elems.length>0) {
                    $(elems[0]).removeClass('to-show');
                }
                $('#hideshow').removeClass('fa-eye').addClass('fa-eye-slash');
                $('#hideshowtxt').html('Hide history');
            }
        }
    </script>
</body>
</html>
        ''' % (content_revision.label, style, content_revision.label,
               content.created.strftime("%B %d, %Y at %H:%m"),
               content.owner.display_name, content_revision.description, disc)

    return thread