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)
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
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))
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
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
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
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
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
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
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
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
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()
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
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
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)
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
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
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__()))
def test_dummy_notifier__notify_content_update(self): c = Content() notifier = DummyNotifier(self.app_config, self.session) notifier.notify_content_update(c)
def unarchive(self, content: Content): content.owner = self._user content.is_archived = False content.revision_type = ActionDescription.UNARCHIVING
def undelete(self, content: Content): content.owner = self._user content.is_deleted = False content.revision_type = ActionDescription.UNDELETION
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